witten
/
luminotes
Archived
1
0
Fork 0

Several changes to handle the case where a note is changed out from under you, due to being saved

from a different window:
 * Made controller.notebooks responsible for preventing unmodified notes from being saved, instead
   of model.Notebook handling this task.
 * Created a revision validator for passing revisions as arguments to exposed methods.
 * controller.Notebooks.save_note() now requires a previous_revision parameter, used to determine
   whether the note has been modified in the particular window it's being saved from.
 * save_note() returns a new previous_revision value, so the client can determine whether a save
   has occurred from another window.
 * controller.Notebooks.undelete_note() fixed to quietly bail if the note to undelete isn't
   actually deleted, which can happen if it was undeleted in another window.
 * Editor() now responsible for making revisions list if it doesn't exist
 * No longer giving an "undo" message when the user deletes an empty note.
 * On the client side, detecting whether the previous_revision as reported by save_note() looks
   correct, and if not, alerting the user about the conflict. Also displaying a "compare versions"
   button that opens both the current version and the previous version.
This commit is contained in:
Dan Helfman 2007-08-23 23:56:42 +00:00
parent 2dcd3e1483
commit 20313728d2
8 changed files with 328 additions and 55 deletions

View File

@ -259,7 +259,23 @@ class Valid_id( object ):
self.__none_okay = none_okay self.__none_okay = none_okay
def __call__( self, value ): def __call__( self, value ):
if self.__none_okay and value in ( None, "" ): return None if self.__none_okay and value in ( None, "None", "" ): return None
if self.ID_PATTERN.search( value ): return str( value ) if self.ID_PATTERN.search( value ): return str( value )
raise ValueError() raise ValueError()
class Valid_revision( object ):
"""
Validator for an object id.
"""
REVISION_PATTERN = re.compile( "^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d+$" )
def __init__( self, none_okay = False ):
self.__none_okay = none_okay
def __call__( self, value ):
if self.__none_okay and value in ( None, "None", "" ): return None
if self.REVISION_PATTERN.search( value ): return str( value )
raise ValueError()

View File

@ -2,7 +2,7 @@ import cherrypy
from Scheduler import Scheduler from Scheduler import Scheduler
from Expose import expose from Expose import expose
from Validate import validate, Valid_string, Validation_error, Valid_bool from Validate import validate, Valid_string, Validation_error, Valid_bool
from Database import Valid_id from Database import Valid_id, Valid_revision
from Users import grab_user_id from Users import grab_user_id
from Updater import wait_for_update, update_client from Updater import wait_for_update, update_client
from Expire import strongly_expire from Expire import strongly_expire
@ -52,7 +52,7 @@ class Notebooks( object ):
@validate( @validate(
notebook_id = Valid_id(), notebook_id = Valid_id(),
note_id = Valid_id(), note_id = Valid_id(),
revision = Valid_string( min = 19, max = 30 ), revision = Valid_revision(),
) )
def default( self, notebook_id, note_id = None, revision = None ): def default( self, notebook_id, note_id = None, revision = None ):
""" """
@ -135,7 +135,7 @@ class Notebooks( object ):
@validate( @validate(
notebook_id = Valid_id(), notebook_id = Valid_id(),
note_id = Valid_id(), note_id = Valid_id(),
revision = Valid_string( min = 19, max = 30 ), revision = Valid_revision(),
user_id = Valid_id( none_okay = True ), user_id = Valid_id( none_okay = True ),
) )
def load_note( self, notebook_id, note_id, revision = None, user_id = None ): def load_note( self, notebook_id, note_id, revision = None, user_id = None ):
@ -269,14 +269,15 @@ class Notebooks( object ):
note_id = Valid_id(), note_id = Valid_id(),
contents = Valid_string( min = 1, max = 25000, escape_html = False ), contents = Valid_string( min = 1, max = 25000, escape_html = False ),
startup = Valid_bool(), startup = Valid_bool(),
previous_revision = Valid_revision( none_okay = True ),
user_id = Valid_id( none_okay = True ), user_id = Valid_id( none_okay = True ),
) )
def save_note( self, notebook_id, note_id, contents, startup, user_id ): def save_note( self, notebook_id, note_id, contents, startup, previous_revision, user_id ):
""" """
Save a new revision of the given note. This function will work both for creating a new note and Save a new revision of the given note. This function will work both for creating a new note and
for updating an existing note. If the note exists and the given contents are identical to the for updating an existing note. If the note exists and the given contents are identical to the
existing contents, then no saving takes place and a new_revision of None is returned. Otherwise existing contents for the given previous_revision, then no saving takes place and a new_revision
this method returns the timestamp of the new revision. of None is returned. Otherwise this method returns the timestamp of the new revision.
@type notebook_id: unicode @type notebook_id: unicode
@param notebook_id: id of notebook the note is in @param notebook_id: id of notebook the note is in
@ -286,10 +287,16 @@ class Notebooks( object ):
@param contents: new textual contents of the note, including its title @param contents: new textual contents of the note, including its title
@type startup: bool @type startup: bool
@param startup: whether the note should be displayed on startup @param startup: whether the note should be displayed on startup
@type previous_revision: unicode or NoneType
@param previous_revision: previous known revision timestamp of the provided note, or None if
the note is new
@type user_id: unicode or NoneType @type user_id: unicode or NoneType
@param user_id: id of current logged-in user (if any), determined by @grab_user_id @param user_id: id of current logged-in user (if any), determined by @grab_user_id
@rtype: json dict @rtype: json dict
@return: { 'new_revision': new revision of saved note, or None } @return: {
'new_revision': new revision of saved note, or None if nothing was saved,
'previous_revision': revision immediately before new_revision, or None if the note is new
}
@raise Access_error: the current user doesn't have access to the given notebook @raise Access_error: the current user doesn't have access to the given notebook
@raise Validation_error: one of the arguments is invalid @raise Validation_error: one of the arguments is invalid
""" """
@ -306,26 +313,40 @@ class Notebooks( object ):
self.__database.load( note_id, self.__scheduler.thread ) self.__database.load( note_id, self.__scheduler.thread )
note = ( yield Scheduler.SLEEP ) note = ( yield Scheduler.SLEEP )
# if the note is already in the database, load it and update it. otherwise, create it # if the note is already in the database, load it and update it
if note and note in notebook.notes: if note and note in notebook.notes:
orig_revision = note.revision # check whether the provided note contents have been changed since the previous revision
notebook.update_note( note, contents ) self.__database.load( note_id, self.__scheduler.thread, previous_revision )
old_note = ( yield Scheduler.SLEEP )
# the note hasn't been changed, so bail without updating it
if contents == old_note.contents:
previous_revision = note.revision
new_revision = None
# the note has changed, so update it
else:
previous_revision = note.revision
notebook.update_note( note, contents )
new_revision = note.revision
# the note is not already in the database, so create it
else: else:
orig_revision = None previous_revision = None
note = Note( note_id, contents ) note = Note( note_id, contents )
notebook.add_note( note ) notebook.add_note( note )
new_revision = note.revision
if startup: if startup:
notebook.add_startup_note( note ) startup_changed = notebook.add_startup_note( note )
else: else:
notebook.remove_startup_note( note ) startup_changed = notebook.remove_startup_note( note )
self.__database.save( notebook ) if new_revision or startup_changed:
self.__database.save( notebook )
if note.revision == orig_revision: yield dict(
yield dict( new_revision = None ) new_revision = new_revision,
else: previous_revision = previous_revision,
yield dict( new_revision = note.revision ) )
@expose( view = Json ) @expose( view = Json )
@wait_for_update @wait_for_update
@ -508,6 +529,12 @@ class Notebooks( object ):
note = ( yield Scheduler.SLEEP ) note = ( yield Scheduler.SLEEP )
if note and notebook.trash: if note and notebook.trash:
# if the note isn't deleted, and it's already in this notebook, just return
if note.deleted_from is None and notebook.lookup_note( note.object_id ):
yield dict()
return
# if the note was deleted from a different notebook than the notebook given, raise
if note.deleted_from != notebook_id: if note.deleted_from != notebook_id:
raise Access_error() raise Access_error()

View File

@ -310,9 +310,11 @@ class Test_notebooks( Test_controller ):
note_id = self.note.object_id, note_id = self.note.object_id,
contents = new_note_contents, contents = new_note_contents,
startup = startup, startup = startup,
previous_revision = previous_revision,
), session_id = self.session_id ) ), session_id = self.session_id )
assert result[ "new_revision" ] and result[ "new_revision" ] != previous_revision assert result[ "new_revision" ] and result[ "new_revision" ] != previous_revision
assert result[ "previous_revision" ] == previous_revision
# 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(
@ -359,6 +361,154 @@ class Test_notebooks( Test_controller ):
def test_save_startup_note_without_login( self ): def test_save_startup_note_without_login( self ):
self.test_save_note_without_login( startup = True ) self.test_save_note_without_login( startup = True )
def test_save_unchanged_note( self, startup = False ):
self.login()
# save over an existing note supplying new contents and a new title
previous_revision = self.note.revision
new_note_contents = u"<h3>new title</h3>new blah"
self.http_post( "/notebooks/save_note/", dict(
notebook_id = self.notebook.object_id,
note_id = self.note.object_id,
contents = new_note_contents,
startup = startup,
previous_revision = previous_revision,
), session_id = self.session_id )
# now attempt to save over that note again without changing the contents
previous_revision = self.note.revision
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 = startup,
previous_revision = previous_revision,
), session_id = self.session_id )
# assert that the note wasn't actually updated the second time
assert result[ "new_revision" ] == None
assert result[ "previous_revision" ] == previous_revision
result = self.http_post( "/notebooks/load_note_by_title/", dict(
notebook_id = self.notebook.object_id,
note_title = "new title",
), session_id = self.session_id )
note = result[ "note" ]
assert note.object_id == self.note.object_id
assert note.title == self.note.title
assert note.contents == self.note.contents
assert note.revision == previous_revision
def test_save_unchanged_note_with_startup_change( self, startup = False ):
self.login()
# save over an existing note supplying new contents and a new title
previous_revision = self.note.revision
new_note_contents = u"<h3>new title</h3>new blah"
self.http_post( "/notebooks/save_note/", dict(
notebook_id = self.notebook.object_id,
note_id = self.note.object_id,
contents = new_note_contents,
startup = startup,
previous_revision = previous_revision,
), session_id = self.session_id )
# now attempt to save over that note again without changing the contents, but with a change
# to its startup flag
previous_revision = self.note.revision
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 = not startup,
previous_revision = previous_revision,
), session_id = self.session_id )
# assert that the note wasn't actually updated the second time
assert result[ "new_revision" ] == None
assert result[ "previous_revision" ] == previous_revision
result = self.http_post( "/notebooks/load_note_by_title/", dict(
notebook_id = self.notebook.object_id,
note_title = "new title",
), session_id = self.session_id )
note = result[ "note" ]
assert note.object_id == self.note.object_id
assert note.title == self.note.title
assert note.contents == self.note.contents
assert note.revision == previous_revision
# assert that the notebook now has the proper startup status for the note
if startup:
assert note not in self.notebook.startup_notes
else:
assert note in self.notebook.startup_notes
def test_save_note_from_an_older_revision( self ):
self.login()
# save over an existing note supplying new contents and a new title
first_revision = self.note.revision
new_note_contents = u"<h3>new title</h3>new 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 = first_revision,
), session_id = self.session_id )
# save over that note again with new contents, providing the original
# revision as the previous known revision
second_revision = self.note.revision
new_note_contents = u"<h3>new new title</h3>new new 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 = first_revision,
), session_id = self.session_id )
# make sure the second save actually caused an update
assert result[ "new_revision" ]
assert result[ "new_revision" ] not in ( first_revision, second_revision )
assert result[ "previous_revision" ] == second_revision
# make sure the first title can no longer be loaded
result = self.http_post( "/notebooks/load_note_by_title/", dict(
notebook_id = self.notebook.object_id,
note_title = "my title",
), session_id = self.session_id )
note = result[ "note" ]
assert note == None
# make sure the second title can no longer be loaded
result = self.http_post( "/notebooks/load_note_by_title/", dict(
notebook_id = self.notebook.object_id,
note_title = "new title",
), session_id = self.session_id )
note = result[ "note" ]
assert note == None
# 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 new title",
), session_id = self.session_id )
note = result[ "note" ]
assert note.object_id == self.note.object_id
assert note.title == self.note.title
assert note.contents == self.note.contents
def test_save_note_with_unknown_notebook( self ): def test_save_note_with_unknown_notebook( self ):
self.login() self.login()
@ -384,9 +534,11 @@ class Test_notebooks( Test_controller ):
note_id = new_note.object_id, note_id = new_note.object_id,
contents = new_note.contents, contents = new_note.contents,
startup = startup, startup = startup,
previous_revision = None,
), session_id = self.session_id ) ), session_id = self.session_id )
assert result[ "new_revision" ] and result[ "new_revision" ] != previous_revision assert result[ "new_revision" ] and result[ "new_revision" ] != previous_revision
assert result[ "previous_revision" ] == None
# 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(
@ -424,9 +576,11 @@ class Test_notebooks( Test_controller ):
note_id = new_note.object_id, note_id = new_note.object_id,
contents = new_note.contents, contents = new_note.contents,
startup = False, startup = False,
previous_revision = None,
), session_id = self.session_id ) ), session_id = self.session_id )
assert result[ "new_revision" ] and result[ "new_revision" ] != previous_revision assert result[ "new_revision" ] and result[ "new_revision" ] != previous_revision
assert result[ "previous_revision" ] == None
# 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(
@ -455,9 +609,11 @@ class Test_notebooks( Test_controller ):
note_id = new_note.object_id, note_id = new_note.object_id,
contents = new_note.contents, contents = new_note.contents,
startup = False, startup = False,
previous_revision = None,
), session_id = self.session_id ) ), session_id = self.session_id )
assert result[ "new_revision" ] and result[ "new_revision" ] != previous_revision assert result[ "new_revision" ] and result[ "new_revision" ] != previous_revision
assert result[ "previous_revision" ] == None
# 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(
@ -713,6 +869,36 @@ class Test_notebooks( Test_controller ):
assert result.get( "note" ) == None assert result.get( "note" ) == None
def test_undelete_note_that_is_not_deleted( self ):
self.login()
# "undelete" the note
result = self.http_post( "/notebooks/undelete_note/", dict(
notebook_id = self.notebook.object_id,
note_id = self.note.object_id,
), session_id = self.session_id )
assert result.get( "error" ) == None
# test that the "undeleted" is where it should be
result = self.http_post( "/notebooks/load_note/", dict(
notebook_id = self.notebook.object_id,
note_id = self.note.object_id,
), session_id = self.session_id )
note = result.get( "note" )
assert note
assert note.object_id == self.note.object_id
assert note.deleted_from == None
# test that the note is not somehow in the trash
result = self.http_post( "/notebooks/load_note/", dict(
notebook_id = self.notebook.trash.object_id,
note_id = self.note.object_id,
), session_id = self.session_id )
assert result.get( "note" ) == None
def test_undelete_note_without_login( self ): def test_undelete_note_without_login( self ):
result = self.http_post( "/notebooks/undelete_note/", dict( result = self.http_post( "/notebooks/undelete_note/", dict(
notebook_id = self.notebook.object_id, notebook_id = self.notebook.object_id,
@ -789,6 +975,26 @@ class Test_notebooks( Test_controller ):
assert note.object_id == self.note.object_id assert note.object_id == self.note.object_id
assert note.deleted_from == self.notebook.object_id assert note.deleted_from == self.notebook.object_id
def test_undelete_note_that_is_not_deleted_from_incorrect_notebook( self ):
self.login()
result = self.http_post( "/notebooks/undelete_note/", dict(
notebook_id = self.anon_notebook,
note_id = self.note.object_id,
), session_id = self.session_id )
assert result.get( "error" )
# test that the note is still in its notebook
result = self.http_post( "/notebooks/load_note/", dict(
notebook_id = self.notebook.object_id,
note_id = self.note.object_id,
), session_id = self.session_id )
note = result.get( "note" )
assert note.object_id == self.note.object_id
assert note.deleted_from == None
def test_blank_note( self ): def test_blank_note( self ):
result = self.http_get( "/notebooks/blank_note/5" ) result = self.http_get( "/notebooks/blank_note/5" )
assert result[ u"id" ] == u"5" assert result[ u"id" ] == u"5"

View File

@ -106,9 +106,6 @@ class Notebook( Persistent ):
if old_note is None: if old_note is None:
raise Notebook.UnknownNoteError( note.object_id ) raise Notebook.UnknownNoteError( note.object_id )
if contents == old_note.contents:
return
self.update_revision() self.update_revision()
self.__titles.pop( note.title, None ) self.__titles.pop( note.title, None )

View File

@ -99,17 +99,6 @@ class Test_notebook( object ):
note = self.notebook.lookup_note_by_title( new_title ) note = self.notebook.lookup_note_by_title( new_title )
assert note == self.note assert note == self.note
def test_update_unrevised_note( self ):
self.notebook.add_note( self.note )
old_title = self.note.title
revision = self.notebook.revision
self.notebook.update_note( self.note, self.note.contents )
assert self.notebook.revision == revision
note = self.notebook.lookup_note( self.note.object_id )
assert note == self.note
@raises( Notebook.UnknownNoteError ) @raises( Notebook.UnknownNoteError )
def test_update_unknown_note( self ): def test_update_unknown_note( self ):
new_contents = u"<h3>new title</h3>new blah" new_contents = u"<h3>new title</h3>new blah"

View File

@ -248,6 +248,7 @@ ol li {
.message_inner { .message_inner {
padding: 0.5em; padding: 0.5em;
line-height: 140%;
background-color: #ffaa44; background-color: #ffaa44;
-moz-border-radius: 0.5em; -moz-border-radius: 0.5em;
-webkit-border-radius: 0.5em; -webkit-border-radius: 0.5em;
@ -266,6 +267,7 @@ ol li {
.error_inner { .error_inner {
padding: 0.5em; padding: 0.5em;
line-height: 140%;
color: #ffffff; color: #ffffff;
background-color: #dd1111; background-color: #dd1111;
-moz-border-radius: 0.5em; -moz-border-radius: 0.5em;

View File

@ -3,7 +3,7 @@ function Editor( id, notebook_id, note_text, deleted_from, revisions_list, inser
this.notebook_id = notebook_id; this.notebook_id = notebook_id;
this.initial_text = note_text; this.initial_text = note_text;
this.deleted_from = deleted_from || null; this.deleted_from = deleted_from || null;
this.revisions_list = revisions_list; this.revisions_list = revisions_list || new Array();
this.read_write = read_write; this.read_write = read_write;
this.startup = startup || false; // whether this Editor is for a startup note this.startup = startup || false; // whether this Editor is for a startup note
this.init_highlight = highlight || false; this.init_highlight = highlight || false;

View File

@ -568,19 +568,19 @@ Wiki.prototype.delete_editor = function ( event, editor ) {
if ( editor == this.focused_editor ) if ( editor == this.focused_editor )
this.focused_editor = null; this.focused_editor = null;
editor.shutdown(); if ( this.notebook.trash && !editor.empty() ) {
} var undo_button = createDOM( "input", {
"type": "button",
"class": "message_button",
"value": "undo",
"title": "undo deletion"
} );
this.display_message( "The note has been moved to the trash.", [ undo_button ] )
var self = this;
connect( undo_button, "onclick", function ( event ) { self.undelete_editor_via_undo( event, editor ); } );
}
if ( this.notebook.trash ) { editor.shutdown();
var undo_button = createDOM( "input", {
"type": "button",
"class": "message_button",
"value": "undo",
"title": "undo deletion"
} );
this.display_message( "The note has been moved to the trash.", [ undo_button ] )
var self = this;
connect( undo_button, "onclick", function ( event ) { self.undelete_editor_via_undo( event, editor ); } );
} }
event.stop(); event.stop();
@ -636,28 +636,60 @@ Wiki.prototype.undelete_editor_via_undo = function( event, editor ) {
event.stop(); event.stop();
} }
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, null, editor.id, previous_revision );
this.load_editor( editor.title, null, editor.id );
}
Wiki.prototype.save_editor = function ( editor, fire_and_forget ) { Wiki.prototype.save_editor = function ( editor, fire_and_forget ) {
if ( !editor ) if ( !editor )
editor = this.focused_editor; editor = this.focused_editor;
var self = this; var self = this;
if ( editor && editor.read_write && !editor.empty() ) { if ( editor && editor.read_write && !editor.empty() ) {
var revisions = editor.revisions_list;
this.invoker.invoke( "/notebooks/save_note", "POST", { this.invoker.invoke( "/notebooks/save_note", "POST", {
"notebook_id": this.notebook_id, "notebook_id": this.notebook_id,
"note_id": editor.id, "note_id": editor.id,
"contents": editor.contents(), "contents": editor.contents(),
"startup": editor.startup "startup": editor.startup,
"previous_revision": revisions.length ? revisions[ revisions.length - 1 ] : "None"
}, function ( result ) { self.update_editor_revisions( result, editor ); }, null, fire_and_forget ); }, function ( result ) { self.update_editor_revisions( result, editor ); }, null, fire_and_forget );
} }
} }
Wiki.prototype.update_editor_revisions = function ( result, editor ) { Wiki.prototype.update_editor_revisions = function ( result, editor ) {
if ( result.new_revision ) { // if there's not a newly saved revision, then the contents are unchanged, so bail
if ( !editor.revisions_list ) if ( !result.new_revision )
editor.revisions_list = new Array(); return;
editor.revisions_list.push( result.new_revision ); var revisions = editor.revisions_list;
var client_previous_revision = revisions.length ? revisions[ revisions.length - 1 ] : null;
// 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 != client_previous_revision ) {
var compare_button = createDOM( "input", {
"type": "button",
"class": "message_button",
"value": "compare versions",
"title": "compare your version with the modified version"
} );
this.display_error( 'Your changes to the note titled "' + editor.title + '" have overwritten changes made in another window.', [ compare_button ] );
var self = this;
connect( compare_button, "onclick", function ( event ) {
self.compare_versions( event, editor, result.previous_revision );
} );
revisions.push( result.previous_revision );
} }
// add the new revision to the editor's revisions list
revisions.push( result.new_revision );
} }
Wiki.prototype.search = function ( event ) { Wiki.prototype.search = function ( event ) {
@ -722,9 +754,8 @@ Wiki.prototype.display_message = function ( text, buttons ) {
this.clear_pulldowns(); this.clear_pulldowns();
var inner_div = DIV( { "class": "message_inner" }, text + " " ); var inner_div = DIV( { "class": "message_inner" }, text + " " );
for ( var i in buttons ) { for ( var i in buttons )
appendChildNodes( inner_div, buttons[ i ] ); appendChildNodes( inner_div, buttons[ i ] );
}
var div = DIV( { "class": "message" }, inner_div ); var div = DIV( { "class": "message" }, inner_div );
div.buttons = buttons; div.buttons = buttons;
@ -733,7 +764,7 @@ Wiki.prototype.display_message = function ( text, buttons ) {
ScrollTo( div ); ScrollTo( div );
} }
Wiki.prototype.display_error = function ( text ) { Wiki.prototype.display_error = function ( text, buttons ) {
this.clear_messages(); this.clear_messages();
this.clear_pulldowns(); this.clear_pulldowns();
@ -745,8 +776,13 @@ Wiki.prototype.display_error = function ( text ) {
editor.shutdown(); editor.shutdown();
} }
var inner_div = DIV( { "class": "error_inner" }, text ); var inner_div = DIV( { "class": "error_inner" }, text + " " );
for ( var i in buttons )
appendChildNodes( inner_div, buttons[ i ] );
var div = DIV( { "class": "error" }, inner_div ); var div = DIV( { "class": "error" }, inner_div );
div.buttons = buttons;
appendChildNodes( "notes", div ); appendChildNodes( "notes", div );
ScrollTo( div ); ScrollTo( div );
} }