diff --git a/controller/Database.py b/controller/Database.py index 141ac79..1f28b20 100644 --- a/controller/Database.py +++ b/controller/Database.py @@ -138,7 +138,7 @@ class Database( object ): print "error unpickling %s: %s" % ( object_id, pickled ) return None self.__cache[ unicode( obj.object_id ).encode( "utf8" ) ] = obj - self.__cache[ unicode( obj.revision_id() ).encode( "utf8" ) ] = obj + self.__cache[ unicode( obj.revision_id() ).encode( "utf8" ) ] = copy( obj ) return obj diff --git a/controller/Html_cleaner.py b/controller/Html_cleaner.py index 0a0a64b..651bfe9 100644 --- a/controller/Html_cleaner.py +++ b/controller/Html_cleaner.py @@ -1,8 +1,8 @@ # originally from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/496942 +import urlparse from htmllib import HTMLParser from cgi import escape -from urlparse import urlparse from formatter import AbstractFormatter, NullWriter from htmlentitydefs import entitydefs from xml.sax.saxutils import quoteattr @@ -120,7 +120,13 @@ class Html_cleaner(HTMLParser): self.handle_endtag(tag, None) def url_is_acceptable(self,url): - parsed = urlparse(url) + parsed = urlparse.urlparse(url) + + # Work-around a nasty bug. urlparse() caches parsed results and returns them on future calls, + # and if the cache isn't cleared here, then a unicode string gets added to the cache, which + # freaks out cherrypy when it independently calls urlparse() with the same URL later. + urlparse.clear_cache() + return parsed[0] in self.allowed_schemes def strip(self, rawstring): diff --git a/controller/Notebooks.py b/controller/Notebooks.py index 3518bff..e20cb6e 100644 --- a/controller/Notebooks.py +++ b/controller/Notebooks.py @@ -39,11 +39,13 @@ class Notebooks( object ): @validate( notebook_id = Valid_id(), note_id = Valid_id(), + revision = Valid_string( min = 19, max = 30 ), ) - def default( self, notebook_id, note_id = None ): + def default( self, notebook_id, note_id = None, revision = None ): return dict( notebook_id = notebook_id, note_id = note_id, + revision = revision, ) @expose( view = Json ) @@ -55,33 +57,10 @@ class Notebooks( object ): @validate( notebook_id = Valid_id(), note_id = Valid_id( none_okay = True ), + revision = Valid_string( min = 0, max = 30 ), user_id = Valid_id( none_okay = True ), ) - def contents( self, notebook_id, note_id = None, user_id = None ): - self.check_access( notebook_id, user_id, self.__scheduler.thread ) - if not ( yield Scheduler.SLEEP ): - raise Access_error() - - self.__database.load( notebook_id, self.__scheduler.thread ) - notebook = ( yield Scheduler.SLEEP ) - - yield dict( - notebook = notebook, - note = notebook.lookup_note( note_id ), - ) - - @expose( view = Json ) - @strongly_expire - @wait_for_update - @grab_user_id - @async - @update_client - @validate( - notebook_id = Valid_id(), - note_id = Valid_id(), - user_id = Valid_id( none_okay = True ), - ) - def load_note( self, notebook_id, note_id, user_id ): + def contents( self, notebook_id, note_id = None, revision = None, user_id = None ): self.check_access( notebook_id, user_id, self.__scheduler.thread ) if not ( yield Scheduler.SLEEP ): raise Access_error() @@ -94,6 +73,44 @@ class Notebooks( object ): else: note = notebook.lookup_note( note_id ) + if revision: + self.__database.load( note_id, self.__scheduler.thread, revision ) + note = ( yield Scheduler.SLEEP ) + + yield dict( + notebook = notebook, + note = note, + ) + + @expose( view = Json ) + @strongly_expire + @wait_for_update + @grab_user_id + @async + @update_client + @validate( + notebook_id = Valid_id(), + note_id = Valid_id(), + revision = Valid_string( min = 19, max = 30 ), + user_id = Valid_id( none_okay = True ), + ) + def load_note( self, notebook_id, note_id, revision = None, user_id = None ): + self.check_access( notebook_id, user_id, self.__scheduler.thread ) + if not ( yield Scheduler.SLEEP ): + raise Access_error() + + self.__database.load( notebook_id, self.__scheduler.thread ) + notebook = ( yield Scheduler.SLEEP ) + + if notebook is None: + note = None + else: + note = notebook.lookup_note( note_id ) + + if revision: + self.__database.load( note_id, self.__scheduler.thread, revision ) + note = ( yield Scheduler.SLEEP ) + yield dict( note = note, ) @@ -264,7 +281,7 @@ class Notebooks( object ): yield dict() @expose( view = Note_page ) - @validate( id = Valid_id() ) + @validate( id = Valid_string( min = 1, max = 100 ) ) def blank_note( self, id ): return dict( id = id ) @@ -382,7 +399,7 @@ class Notebooks( object ): self.__database.load( user_id, self.__scheduler.thread ) user = ( yield Scheduler.SLEEP ) - if user.has_access( notebook_id ): + if user and user.has_access( notebook_id ): access = True yield callback, access diff --git a/controller/test/Test_database.py b/controller/test/Test_database.py index 60961da..08923ea 100644 --- a/controller/test/Test_database.py +++ b/controller/test/Test_database.py @@ -37,6 +37,7 @@ class Test_database( object ): def test_save_and_load( self ): def gen(): basic_obj = Some_object( object_id = "5", value = 1 ) + original_revision = basic_obj.revision self.database.save( basic_obj, self.scheduler.thread ) yield Scheduler.SLEEP @@ -45,6 +46,8 @@ class Test_database( object ): obj = ( yield Scheduler.SLEEP ) assert obj.object_id == basic_obj.object_id + assert obj.revision == original_revision + assert obj.revisions_list == [ original_revision ] assert obj.value == basic_obj.value g = gen() @@ -54,7 +57,9 @@ class Test_database( object ): def test_complex_save_and_load( self ): def gen(): basic_obj = Some_object( object_id = "7", value = 2 ) + basic_original_revision = basic_obj.revision complex_obj = Some_object( object_id = "6", value = basic_obj ) + complex_original_revision = complex_obj.revision self.database.save( complex_obj, self.scheduler.thread ) yield Scheduler.SLEEP @@ -64,14 +69,20 @@ class Test_database( object ): if self.clear_cache: self.database.clear_cache() assert obj.object_id == complex_obj.object_id + assert obj.revision == complex_original_revision + assert obj.revisions_list == [ complex_original_revision ] assert obj.value.object_id == basic_obj.object_id assert obj.value.value == basic_obj.value + assert obj.value.revision == basic_original_revision + assert obj.value.revisions_list == [ basic_original_revision ] self.database.load( basic_obj.object_id, self.scheduler.thread ) obj = ( yield Scheduler.SLEEP ) assert obj.object_id == basic_obj.object_id assert obj.value == basic_obj.value + assert obj.revision == basic_original_revision + assert obj.revisions_list == [ basic_original_revision ] g = gen() self.scheduler.add( g ) @@ -80,6 +91,7 @@ class Test_database( object ): def test_save_and_load_by_secondary( self ): def gen(): basic_obj = Some_object( object_id = "5", value = 1, secondary_id = u"foo" ) + original_revision = basic_obj.revision self.database.save( basic_obj, self.scheduler.thread ) yield Scheduler.SLEEP @@ -89,6 +101,8 @@ class Test_database( object ): assert obj.object_id == basic_obj.object_id assert obj.value == basic_obj.value + assert obj.revision == original_revision + assert obj.revisions_list == [ original_revision ] g = gen() self.scheduler.add( g ) @@ -97,7 +111,9 @@ class Test_database( object ): def test_duplicate_save_and_load( self ): def gen(): basic_obj = Some_object( object_id = "9", value = 3 ) + basic_original_revision = basic_obj.revision complex_obj = Some_object( object_id = "8", value = basic_obj, value2 = basic_obj ) + complex_original_revision = complex_obj.revision self.database.save( complex_obj, self.scheduler.thread ) yield Scheduler.SLEEP @@ -107,10 +123,19 @@ class Test_database( object ): if self.clear_cache: self.database.clear_cache() assert obj.object_id == complex_obj.object_id + assert obj.revision == complex_original_revision + assert obj.revisions_list == [ complex_original_revision ] + assert obj.value.object_id == basic_obj.object_id assert obj.value.value == basic_obj.value + assert obj.value.revision == basic_original_revision + assert obj.value.revisions_list == [ basic_original_revision ] + assert obj.value2.object_id == basic_obj.object_id assert obj.value2.value == basic_obj.value + assert obj.value2.revision == basic_original_revision + assert obj.value2.revisions_list == [ basic_original_revision ] + assert obj.value == obj.value2 self.database.load( basic_obj.object_id, self.scheduler.thread ) @@ -118,6 +143,8 @@ class Test_database( object ): assert obj.object_id == basic_obj.object_id assert obj.value == basic_obj.value + assert obj.revision == basic_original_revision + assert obj.revisions_list == [ basic_original_revision ] g = gen() self.scheduler.add( g ) @@ -143,14 +170,17 @@ class Test_database( object ): assert obj.object_id == basic_obj.object_id assert obj.revision == basic_obj.revision + assert obj.revisions_list == [ original_revision, basic_obj.revision ] assert obj.value == basic_obj.value self.database.load( basic_obj.object_id, self.scheduler.thread, revision = original_revision ) - obj = ( yield Scheduler.SLEEP ) + revised = ( yield Scheduler.SLEEP ) - assert obj.object_id == basic_obj.object_id - assert obj.revision == original_revision - assert obj.value == 1 + assert revised.object_id == basic_obj.object_id + assert revised.value == 1 + assert revised.revision == original_revision + assert id( obj.revisions_list ) != id( revised.revisions_list ) + assert revised.revisions_list == [ original_revision ] g = gen() self.scheduler.add( g ) @@ -171,6 +201,7 @@ class Test_database( object ): def test_reload( self ): def gen(): basic_obj = Some_object( object_id = "5", value = 1 ) + original_revision = basic_obj.revision self.database.save( basic_obj, self.scheduler.thread ) yield Scheduler.SLEEP @@ -192,6 +223,8 @@ class Test_database( object ): assert obj.object_id == basic_obj.object_id assert obj.value == 55 + assert obj.revision == original_revision + assert obj.revisions_list == [ original_revision ] g = gen() self.scheduler.add( g ) @@ -229,6 +262,7 @@ class Test_database( object ): assert obj.object_id == basic_obj.object_id assert obj.revision == original_revision + assert obj.revisions_list == [ original_revision ] assert obj.value == 55 g = gen() diff --git a/controller/test/Test_notebooks.py b/controller/test/Test_notebooks.py index 2897900..69e4ec0 100644 --- a/controller/test/Test_notebooks.py +++ b/controller/test/Test_notebooks.py @@ -1,5 +1,6 @@ import cherrypy import cgi +from urllib import quote from Test_controller import Test_controller from controller.Scheduler import Scheduler from model.Notebook import Notebook @@ -68,6 +69,17 @@ class Test_notebooks( Test_controller ): assert result.get( u"notebook_id" ) == self.notebook.object_id assert result.get( u"note_id" ) == self.note.object_id + def test_default_with_note_and_revision( self ): + result = self.http_get( "/notebooks/%s?note_id=%s&revision=%s" % ( + self.notebook.object_id, + self.note.object_id, + quote( unicode( self.note.revision ) ), + ) ) + + assert result.get( u"notebook_id" ) == self.notebook.object_id + assert result.get( u"note_id" ) == self.note.object_id + assert result.get( u"revision" ) == unicode( self.note.revision ) + def test_contents( self ): self.login() @@ -100,6 +112,28 @@ class Test_notebooks( Test_controller ): assert note.object_id == self.note.object_id + def test_contents_with_note_and_revision( self ): + self.login() + + result = self.http_get( + "/notebooks/contents?notebook_id=%s¬e_id=%s&revision=%s" % ( + self.notebook.object_id, + self.note.object_id, + quote( unicode( self.note.revision ) ), + ), + session_id = self.session_id, + ) + + notebook = result[ "notebook" ] + + assert notebook.object_id == self.notebook.object_id + assert len( notebook.startup_notes ) == 1 + assert notebook.startup_notes[ 0 ] == self.note + + note = result[ "note" ] + + assert note.object_id == self.note.object_id + def test_contents_without_login( self ): result = self.http_get( "/notebooks/contents?notebook_id=%s" % self.notebook.object_id, @@ -122,6 +156,35 @@ class Test_notebooks( Test_controller ): assert note.title == self.note.title assert note.contents == self.note.contents + def test_load_note_with_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"

new title

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, + ), session_id = self.session_id ) + + # load the note by the old revision + result = self.http_post( "/notebooks/load_note/", dict( + notebook_id = self.notebook.object_id, + note_id = self.note.object_id, + revision = previous_revision, + ), session_id = self.session_id ) + + note = result[ "note" ] + + # assert that we get the previous revision of the note, not the new one + assert note.object_id == self.note.object_id + assert note.revision == previous_revision + assert note.title == previous_title + assert note.contents == previous_contents + def test_load_note_without_login( self ): result = self.http_post( "/notebooks/load_note/", dict( notebook_id = self.notebook.object_id, @@ -510,6 +573,7 @@ class Test_notebooks( Test_controller ): note_id = self.note.object_id, ), session_id = self.session_id ) + print result assert result.get( "note" ) == None def test_delete_note_without_login( self ): diff --git a/model/Note.py b/model/Note.py index 5488866..0a21db0 100644 --- a/model/Note.py +++ b/model/Note.py @@ -24,10 +24,11 @@ class Note( Persistent ): self.__title = None self.__contents = None or "" - self.__set_contents( contents ) + self.__set_contents( contents, new_revision = False ) - def __set_contents( self, contents ): - self.update_revision() + def __set_contents( self, contents, new_revision = True ): + if new_revision: + self.update_revision() self.__contents = contents # parse title out of the beginning of the contents diff --git a/model/Persistent.py b/model/Persistent.py index 9a41fc6..954087b 100644 --- a/model/Persistent.py +++ b/model/Persistent.py @@ -10,7 +10,9 @@ class Persistent( object ): def update_revision( self ): self.__revision = datetime.now() - self.__revisions_list.append( self.__revision ) + + # make a new copy of the list to prevent sharing of this list between different revisions + self.__revisions_list = self.__revisions_list + [ self.__revision ] def revision_id( self ): return "%s %s" % ( self.__object_id, self.__revision ) diff --git a/static/css/note.css b/static/css/note.css index ca4742c..103acdc 100644 --- a/static/css/note.css +++ b/static/css/note.css @@ -27,6 +27,10 @@ h3 { -webkit-border-radius: 0.5em; } +.small_text { + font-size: 0.75em; +} + ul li { margin-top: 0.5em; } diff --git a/static/html/features.html b/static/html/features.html index a2dfa49..a297d6e 100644 --- a/static/html/features.html +++ b/static/html/features.html @@ -33,5 +33,5 @@ A single search looks through every word in the entire wiki.

-Sound interesting? Then take a tour or try it out for yourself! +Sound interesting? Then take a tour or try it out for yourself!

diff --git a/static/html/login.html b/static/html/login.html index 6c21d1e..9b2baca 100644 --- a/static/html/login.html +++ b/static/html/login.html @@ -1,6 +1,6 @@

login

-No account yet? Want to make a wiki? You can try it out for free. +No account yet? Want to make a wiki? You can try it out for free.

@@ -18,6 +18,6 @@ No account yet? Want to make a wiki? You can try it out

-Forgot? Need your password reset? +Forgot? Need your password reset?

diff --git a/static/html/navigation.html b/static/html/navigation.html index d3a3c7a..acf0482 100644 --- a/static/html/navigation.html +++ b/static/html/navigation.html @@ -1,5 +1,5 @@ -about - -features - -take a tour - -try it out - -login +about - +features - +take a tour - +try it out - +login diff --git a/static/html/supported browsers.html b/static/html/supported browsers.html index d24dd99..e3e51dd 100644 --- a/static/html/supported browsers.html +++ b/static/html/supported browsers.html @@ -1,7 +1,7 @@

supported browsers

-Luminotes makes use of some advanced browser features, +Luminotes makes use of some advanced browser features, so not all browsers will work for editing your wiki. Supported browsers include:

diff --git a/static/html/try it out.html b/static/html/try it out.html index f76ca72..27f58cb 100644 --- a/static/html/try it out.html +++ b/static/html/try it out.html @@ -35,5 +35,5 @@ dream of giving out your email address.

-Please make sure you're using one of the supported browsers. +Please make sure you're using one of the supported browsers. diff --git a/static/js/Editor.js b/static/js/Editor.js index 539ae0e..28e243a 100644 --- a/static/js/Editor.js +++ b/static/js/Editor.js @@ -126,13 +126,13 @@ Editor.prototype.finish_init = function () { if ( this.read_write ) { connect( this.document, "onkeydown", function ( event ) { self.key_pressed( event ); } ); connect( this.document, "onkeyup", function ( event ) { self.key_released( event ); } ); + connect( this.document, "onblur", function ( event ) { self.blurred( event ); } ); + connect( this.document, "onfocus", function ( event ) { self.focused( event ); } ); + connect( this.document.body, "onblur", function ( event ) { self.blurred( event ); } ); + connect( this.document.body, "onfocus", function ( event ) { self.focused( event ); } ); } connect( this.document, "onclick", function ( event ) { self.mouse_clicked( event ); } ); - connect( this.document, "onblur", function ( event ) { self.blurred( event ); } ); - connect( this.document, "onfocus", function ( event ) { self.focused( event ); } ); - connect( this.document.body, "onblur", function ( event ) { self.blurred( event ); } ); - connect( this.document.body, "onfocus", function ( event ) { self.focused( event ); } ); // special-case: connect any submit buttons within the contents of this note var signup_button = withDocument( this.document, function () { return getElement( "signup_button" ); } ); @@ -431,6 +431,8 @@ Editor.prototype.focus = function () { // return true if the specified state is enabled Editor.prototype.state_enabled = function ( state_name ) { + if ( !this.read_write ) return false; + state_name = state_name.toLowerCase(); var format_block = this.document.queryCommandValue( "formatblock" ).toLowerCase(); var heading = ( format_block == "h3" || format_block == "heading 3" ); diff --git a/static/js/Wiki.js b/static/js/Wiki.js index e7607aa..ef123d2 100644 --- a/static/js/Wiki.js +++ b/static/js/Wiki.js @@ -17,7 +17,8 @@ function Wiki() { this.invoker.invoke( "/notebooks/contents", "GET", { "notebook_id": this.notebook_id, - "note_id": getElement( "note_id" ).value + "note_id": getElement( "note_id" ).value, + "revision": getElement( "revision" ).value }, function( result ) { self.populate( result ); } ); @@ -126,13 +127,15 @@ Wiki.prototype.populate = function ( result ) { // don't actually create an editor if a particular note was provided in the result if ( !result.note ) { var focus = ( i == 0 ); - this.create_editor( note.object_id, note.contents, note.revisions_list, undefined, undefined, false, focus ); + this.create_editor( note.object_id, note.contents, note.revisions_list, undefined, undefined, this.read_write, false, focus ); } } // if one particular note was provided, then just display an editor for that note + var read_write = this.read_write; + if ( getElement( "revision" ).value ) read_write = false; if ( result.note ) - this.create_editor( result.note.object_id, result.note.contents, result.note.revisions_list, undefined, undefined, false, true ); + this.create_editor( result.note.object_id, result.note.contents, result.note.revisions_list, undefined, undefined, read_write, false, true ); } Wiki.prototype.background_clicked = function ( event ) { @@ -160,18 +163,19 @@ Wiki.prototype.create_blank_editor = function ( event ) { } } - this.blank_editor_id = this.create_editor( undefined, undefined, undefined, undefined, undefined, true, true ); + this.blank_editor_id = this.create_editor( undefined, undefined, undefined, undefined, undefined, this.read_write, true, true ); } -Wiki.prototype.load_editor = function ( note_title, insert_after_iframe_id, note_id ) { +Wiki.prototype.load_editor = function ( note_title, insert_after_iframe_id, note_id, revision ) { var self = this; this.invoker.invoke( "/notebooks/load_note", "GET", { "notebook_id": this.notebook_id, - "note_id": note_id + "note_id": note_id, + "revision": revision }, - function ( result ) { self.parse_loaded_editor( result, insert_after_iframe_id, note_title ); } + function ( result ) { self.parse_loaded_editor( result, insert_after_iframe_id, note_title, revision ); } ); } @@ -187,9 +191,10 @@ Wiki.prototype.load_editor_by_title = function ( note_title, insert_after_iframe ); } -Wiki.prototype.parse_loaded_editor = function ( result, insert_after_iframe_id, note_title ) { +Wiki.prototype.parse_loaded_editor = function ( result, insert_after_iframe_id, note_title, revision ) { if ( result.note ) { - var id = result.note.object_id + var id = result.note.object_id; + if ( revision ) id += " " + revision; var note_text = result.note.contents; var revisions_list = result.note.revisions_list; } else { @@ -198,10 +203,15 @@ Wiki.prototype.parse_loaded_editor = function ( result, insert_after_iframe_id, var revisions_list = new Array(); } - this.create_editor( id, note_text, revisions_list, insert_after_iframe_id, note_title, true, false ); + if ( revision ) + var read_write = false; // show previous revisions as read-only + else + var read_write = this.read_write; + + this.create_editor( id, note_text, revisions_list, insert_after_iframe_id, note_title, read_write, true, false ); } -Wiki.prototype.create_editor = function ( id, note_text, revisions_list, insert_after_iframe_id, note_title, highlight, focus ) { +Wiki.prototype.create_editor = function ( id, note_text, revisions_list, insert_after_iframe_id, note_title, read_write, highlight, focus ) { this.clear_messages(); this.clear_pulldowns(); @@ -240,8 +250,14 @@ Wiki.prototype.create_editor = function ( id, note_text, revisions_list, insert_ } } + // for read-only notes within read-write notebooks, tack the revision timestamp onto the start of the note text + if ( !read_write && this.read_write && revisions_list && revisions_list.length ) { + var short_revision = this.brief_revision( revisions_list[ revisions_list.length - 1 ] ); + note_text = "

Previous revision from " + short_revision + "

" + note_text; + } + var startup = this.startup_notes[ id ]; - var editor = new Editor( id, this.notebook_id, note_text, revisions_list, undefined, this.read_write, startup, highlight, focus ); + var editor = new Editor( id, this.notebook_id, note_text, revisions_list, undefined, read_write, startup, highlight, focus ); if ( this.read_write ) { connect( editor, "state_changed", this, "editor_state_changed" ); @@ -336,7 +352,7 @@ Wiki.prototype.toggle_button = function ( event, button_id, state_name ) { this.clear_messages(); this.clear_pulldowns(); - if ( this.focused_editor ) { + if ( this.focused_editor && this.focused_editor.read_write ) { this.focused_editor.focus(); this.focused_editor.exec_command( state_name || button_id ); this.focused_editor.resize(); @@ -368,7 +384,7 @@ Wiki.prototype.toggle_link_button = function ( event ) { this.clear_messages(); this.clear_pulldowns(); - if ( this.focused_editor ) { + if ( this.focused_editor && this.focused_editor.read_write ) { this.focused_editor.focus(); toggleElementClass( "button_down", "createLink" ); if ( hasElementClass( "createLink", "button_down" ) ) @@ -413,7 +429,7 @@ Wiki.prototype.delete_editor = function ( event, editor ) { if ( this.startup_notes[ editor.id ] ) delete this.startup_notes[ editor.id ]; - if ( this.read_write ) { + if ( this.read_write && editor.read_write ) { this.invoker.invoke( "/notebooks/delete_note", "POST", { "notebook_id": this.notebook_id, "note_id": editor.id @@ -434,8 +450,7 @@ Wiki.prototype.save_editor = function ( editor, fire_and_forget ) { editor = this.focused_editor; var self = this; - if ( editor && !editor.empty() ) { - // TODO: do something with the result other than just ignoring it + if ( editor && editor.read_write && !editor.empty() ) { this.invoker.invoke( "/notebooks/save_note", "POST", { "notebook_id": this.notebook_id, "note_id": editor.id, @@ -494,7 +509,7 @@ Wiki.prototype.display_search_results = function ( result ) { continue; } - this.create_editor( note.object_id, note.contents, note.revisions_list, undefined, undefined, false, focus ); + this.create_editor( note.object_id, note.contents, note.revisions_list, undefined, undefined, this.read_write, false, focus ); } } @@ -530,6 +545,10 @@ Wiki.prototype.clear_pulldowns = function () { } } +Wiki.prototype.brief_revision = function ( revision ) { + return revision.split( /\.\d/ )[ 0 ]; // strip off seconds from the timestamp +} + Wiki.prototype.toggle_editor_changes = function ( event, editor ) { // if the pulldown is already open, then just close it var pulldown_id = "changes_" + editor.id; @@ -539,7 +558,7 @@ Wiki.prototype.toggle_editor_changes = function ( event, editor ) { return; } - new Changes_pulldown( this.notebook_id, this.invoker, editor ); + new Changes_pulldown( this, this.notebook_id, this.invoker, editor ); event.stop(); } @@ -552,14 +571,15 @@ Wiki.prototype.toggle_editor_options = function ( event, editor ) { return; } - new Options_pulldown( this.notebook_id, this.invoker, editor ); + new Options_pulldown( this, this.notebook_id, this.invoker, editor ); event.stop(); } connect( window, "onload", function ( event ) { new Wiki(); } ); -function Pulldown( notebook_id, pulldown_id, button ) { +function Pulldown( wiki, notebook_id, pulldown_id, button ) { + this.wiki = wiki; this.notebook_id = notebook_id; this.div = createDOM( "div", { "id": pulldown_id, "class": "pulldown" } ); this.div.pulldown = this; @@ -584,8 +604,8 @@ Pulldown.prototype.shutdown = function () { } -function Options_pulldown( notebook_id, invoker, editor ) { - Pulldown.call( this, notebook_id, "options_" + editor.id, editor.options_button ); +function Options_pulldown( wiki, notebook_id, invoker, editor ) { + Pulldown.call( this, wiki, notebook_id, "options_" + editor.id, editor.options_button ); this.invoker = invoker; this.editor = editor; @@ -611,32 +631,27 @@ Options_pulldown.prototype.startup_clicked = function ( event ) { this.startup_checkbox.checked = this.startup_checkbox.checked ? false : true; this.editor.startup = this.startup_checkbox.checked; - // if this note isn't empty, save it along with its startup status - if ( !this.editor.empty() ) { - this.invoker.invoke( "/notebooks/save_note", "POST", { - "notebook_id": this.notebook_id, - "note_id": this.editor.id, - "contents": this.editor.contents(), - "startup": this.editor.startup - } ); - } + // save this note along with its toggled startup state + this.wiki.save_editor( this.editor ); } Options_pulldown.prototype.shutdown = function () { Pulldown.prototype.shutdown.call( this ); + disconnectAll( this.startup_checkbox ); disconnectAll( this.startup_toggle ); } -function Changes_pulldown( notebook_id, invoker, editor ) { - Pulldown.call( this, notebook_id, "changes_" + editor.id, editor.changes_button ); +function Changes_pulldown( wiki, notebook_id, invoker, editor ) { + Pulldown.call( this, wiki, notebook_id, "changes_" + editor.id, editor.changes_button ); this.invoker = invoker; this.editor = editor; + this.links = new Array(); // display list of revision timestamps in reverse chronological order - if ( isUndefinedOrNull( this.editor.revisions_list ) ) { + if ( isUndefinedOrNull( this.editor.revisions_list ) || this.editor.revisions_list.length == 0 ) { appendChildNodes( this.div, createDOM( "span", "This note has no previous changes." ) ); return; } @@ -644,18 +659,35 @@ function Changes_pulldown( notebook_id, invoker, editor ) { var revisions_list = clone( this.editor.revisions_list ); revisions_list.reverse(); + var self = this; for ( var i = 0; i < revisions_list.length; ++i ) { var revision = revisions_list[ i ]; - revision = revision.split( /\.\d/ )[ 0 ]; // strip off seconds from the timestamp + var short_revision = this.wiki.brief_revision( revision ); var href = "/notebooks/" + this.notebook_id + "?" + queryString( [ "note_id", "revision" ], [ this.editor.id, revision ] ); -// appendChildNodes( this.div, createDOM( "a", { "href": href, "class": "pulldown_link" }, revision ) ); - appendChildNodes( this.div, createDOM( "span", {}, revision ) ); + var link = createDOM( "a", { "href": href, "class": "pulldown_link" }, short_revision ); + this.links.push( link ); + link.revision = revision; + connect( link, "onclick", function ( event ) { self.link_clicked( event, self.editor.id ); } ); + appendChildNodes( this.div, link ); appendChildNodes( this.div, createDOM( "br" ) ); } } Changes_pulldown.prototype = Pulldown; 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.", null, note_id, revision ); + event.stop(); +} + +Options_pulldown.prototype.shutdown = function () { + Pulldown.prototype.shutdown.call( this ); + + for ( var i in this.links ) + disconnectAll( this.links[ i ] ); +} diff --git a/tools/initdb.py b/tools/initdb.py index 4acde4c..dd1e678 100644 --- a/tools/initdb.py +++ b/tools/initdb.py @@ -52,7 +52,7 @@ class Initializer( object ): for ( filename, startup ) in self.ENTRY_FILES: full_filename = os.path.join( self.HTML_PATH, filename ) - contents = file( full_filename ).read() + contents = file( full_filename ).read().replace( "%s", main_notebook_id ) self.database.next_id( self.scheduler.thread ) note_id = ( yield Scheduler.SLEEP ) diff --git a/tools/updatedb.py b/tools/updatedb.py index 32c1dad..644d08e 100755 --- a/tools/updatedb.py +++ b/tools/updatedb.py @@ -34,7 +34,7 @@ class Initializer( object ): self.scheduler.wait_for( thread ) def update_main_notebook( self ): - self.database.load( u"anonymous", self.scheduler.thread ) + self.database.load( u"User anonymous", self.scheduler.thread ) anonymous = ( yield Scheduler.SLEEP ) main_notebook = anonymous.notebooks[ 0 ]._Read_only_notebook__wrapped startup_notes = [] @@ -42,7 +42,7 @@ class Initializer( object ): # update all of the notes in the main notebook for ( filename, startup ) in self.ENTRY_FILES: full_filename = os.path.join( self.HTML_PATH, filename ) - contents = file( full_filename ).read() + contents = file( full_filename ).read().replace( "%s", main_notebook.object_id ) title = filename.replace( u".html", u"" ) note = main_notebook.lookup_note_by_title( title ) diff --git a/view/Main_page.py b/view/Main_page.py index 89495bb..7989e56 100644 --- a/view/Main_page.py +++ b/view/Main_page.py @@ -6,7 +6,7 @@ from Toolbar import Toolbar class Main_page( Page ): - def __init__( self, notebook_id = None, note_id = None ): + def __init__( self, notebook_id = None, note_id = None, revision = None ): title = None Page.__init__( @@ -14,6 +14,7 @@ class Main_page( Page ): title, Input( type = u"hidden", name = u"notebook_id", id = u"notebook_id", value = notebook_id or "" ), Input( type = u"hidden", name = u"note_id", id = u"note_id", value = note_id or "" ), + Input( type = u"hidden", name = u"revision", id = u"revision", value = revision or "" ), Div( id = u"status_area", ),