witten
/
luminotes
Archived
1
0
Fork 0

* Can now click on revision timestamps to open up the contents of previous note revisions with a small timestamp at the top.

* Revisions can be opened either in the current page or in a new window/tab.
 * Added ability for a read-write notebook to contain read-only notes. This supports showing read-only revisions.
 * Fixed updatedb.py to properly load the anonymous user.
 * Updated initdb.py and updatedb.py to deadl with new-style /notebooks/notebookid?note_id=noteid wiki links.
 * Made Persistent copy the revisions_list on each revision update so different revisions don't share lists.
 * Prevented Note from updating its revision twice upon construction. Now it's only updated once.
 * Work-around for nasty urlparse() caching bug related to unicode strings that cherrypy barfs on.
 * Added optional revision flag to various controller.Notebooks methods to allow opening of a notebook with a particular note revision displayed.
This commit is contained in:
Dan Helfman 2007-07-31 22:53:57 +00:00
parent b7b88f25a3
commit f23fcdde21
18 changed files with 258 additions and 95 deletions

View File

@ -138,7 +138,7 @@ class Database( object ):
print "error unpickling %s: %s" % ( object_id, pickled ) print "error unpickling %s: %s" % ( object_id, pickled )
return None return None
self.__cache[ unicode( obj.object_id ).encode( "utf8" ) ] = obj 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 return obj

View File

@ -1,8 +1,8 @@
# originally from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/496942 # originally from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/496942
import urlparse
from htmllib import HTMLParser from htmllib import HTMLParser
from cgi import escape from cgi import escape
from urlparse import urlparse
from formatter import AbstractFormatter, NullWriter from formatter import AbstractFormatter, NullWriter
from htmlentitydefs import entitydefs from htmlentitydefs import entitydefs
from xml.sax.saxutils import quoteattr from xml.sax.saxutils import quoteattr
@ -120,7 +120,13 @@ class Html_cleaner(HTMLParser):
self.handle_endtag(tag, None) self.handle_endtag(tag, None)
def url_is_acceptable(self,url): 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 return parsed[0] in self.allowed_schemes
def strip(self, rawstring): def strip(self, rawstring):

View File

@ -39,11 +39,13 @@ 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 ),
) )
def default( self, notebook_id, note_id = None ): def default( self, notebook_id, note_id = None, revision = None ):
return dict( return dict(
notebook_id = notebook_id, notebook_id = notebook_id,
note_id = note_id, note_id = note_id,
revision = revision,
) )
@expose( view = Json ) @expose( view = Json )
@ -55,33 +57,10 @@ class Notebooks( object ):
@validate( @validate(
notebook_id = Valid_id(), notebook_id = Valid_id(),
note_id = Valid_id( none_okay = True ), note_id = Valid_id( none_okay = True ),
revision = Valid_string( min = 0, max = 30 ),
user_id = Valid_id( none_okay = True ), user_id = Valid_id( none_okay = True ),
) )
def contents( self, notebook_id, note_id = None, user_id = None ): 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()
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 ):
self.check_access( notebook_id, user_id, self.__scheduler.thread ) self.check_access( notebook_id, user_id, self.__scheduler.thread )
if not ( yield Scheduler.SLEEP ): if not ( yield Scheduler.SLEEP ):
raise Access_error() raise Access_error()
@ -94,6 +73,44 @@ class Notebooks( object ):
else: else:
note = notebook.lookup_note( note_id ) 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( yield dict(
note = note, note = note,
) )
@ -264,7 +281,7 @@ class Notebooks( object ):
yield dict() yield dict()
@expose( view = Note_page ) @expose( view = Note_page )
@validate( id = Valid_id() ) @validate( id = Valid_string( min = 1, max = 100 ) )
def blank_note( self, id ): def blank_note( self, id ):
return dict( id = id ) return dict( id = id )
@ -382,7 +399,7 @@ class Notebooks( object ):
self.__database.load( user_id, self.__scheduler.thread ) self.__database.load( user_id, self.__scheduler.thread )
user = ( yield Scheduler.SLEEP ) user = ( yield Scheduler.SLEEP )
if user.has_access( notebook_id ): if user and user.has_access( notebook_id ):
access = True access = True
yield callback, access yield callback, access

View File

@ -37,6 +37,7 @@ class Test_database( object ):
def test_save_and_load( self ): def test_save_and_load( self ):
def gen(): def gen():
basic_obj = Some_object( object_id = "5", value = 1 ) basic_obj = Some_object( object_id = "5", value = 1 )
original_revision = basic_obj.revision
self.database.save( basic_obj, self.scheduler.thread ) self.database.save( basic_obj, self.scheduler.thread )
yield Scheduler.SLEEP yield Scheduler.SLEEP
@ -45,6 +46,8 @@ class Test_database( object ):
obj = ( yield Scheduler.SLEEP ) obj = ( yield Scheduler.SLEEP )
assert obj.object_id == basic_obj.object_id 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 assert obj.value == basic_obj.value
g = gen() g = gen()
@ -54,7 +57,9 @@ class Test_database( object ):
def test_complex_save_and_load( self ): def test_complex_save_and_load( self ):
def gen(): def gen():
basic_obj = Some_object( object_id = "7", value = 2 ) 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_obj = Some_object( object_id = "6", value = basic_obj )
complex_original_revision = complex_obj.revision
self.database.save( complex_obj, self.scheduler.thread ) self.database.save( complex_obj, self.scheduler.thread )
yield Scheduler.SLEEP yield Scheduler.SLEEP
@ -64,14 +69,20 @@ class Test_database( object ):
if self.clear_cache: self.database.clear_cache() if self.clear_cache: self.database.clear_cache()
assert obj.object_id == complex_obj.object_id 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.object_id == basic_obj.object_id
assert obj.value.value == basic_obj.value 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 ) self.database.load( basic_obj.object_id, self.scheduler.thread )
obj = ( yield Scheduler.SLEEP ) obj = ( yield Scheduler.SLEEP )
assert obj.object_id == basic_obj.object_id assert obj.object_id == basic_obj.object_id
assert obj.value == basic_obj.value assert obj.value == basic_obj.value
assert obj.revision == basic_original_revision
assert obj.revisions_list == [ basic_original_revision ]
g = gen() g = gen()
self.scheduler.add( g ) self.scheduler.add( g )
@ -80,6 +91,7 @@ class Test_database( object ):
def test_save_and_load_by_secondary( self ): def test_save_and_load_by_secondary( self ):
def gen(): def gen():
basic_obj = Some_object( object_id = "5", value = 1, secondary_id = u"foo" ) 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 ) self.database.save( basic_obj, self.scheduler.thread )
yield Scheduler.SLEEP yield Scheduler.SLEEP
@ -89,6 +101,8 @@ class Test_database( object ):
assert obj.object_id == basic_obj.object_id assert obj.object_id == basic_obj.object_id
assert obj.value == basic_obj.value assert obj.value == basic_obj.value
assert obj.revision == original_revision
assert obj.revisions_list == [ original_revision ]
g = gen() g = gen()
self.scheduler.add( g ) self.scheduler.add( g )
@ -97,7 +111,9 @@ class Test_database( object ):
def test_duplicate_save_and_load( self ): def test_duplicate_save_and_load( self ):
def gen(): def gen():
basic_obj = Some_object( object_id = "9", value = 3 ) 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_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 ) self.database.save( complex_obj, self.scheduler.thread )
yield Scheduler.SLEEP yield Scheduler.SLEEP
@ -107,10 +123,19 @@ class Test_database( object ):
if self.clear_cache: self.database.clear_cache() if self.clear_cache: self.database.clear_cache()
assert obj.object_id == complex_obj.object_id 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.object_id == basic_obj.object_id
assert obj.value.value == basic_obj.value 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.object_id == basic_obj.object_id
assert obj.value2.value == basic_obj.value 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 assert obj.value == obj.value2
self.database.load( basic_obj.object_id, self.scheduler.thread ) 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.object_id == basic_obj.object_id
assert obj.value == basic_obj.value assert obj.value == basic_obj.value
assert obj.revision == basic_original_revision
assert obj.revisions_list == [ basic_original_revision ]
g = gen() g = gen()
self.scheduler.add( g ) self.scheduler.add( g )
@ -143,14 +170,17 @@ class Test_database( object ):
assert obj.object_id == basic_obj.object_id assert obj.object_id == basic_obj.object_id
assert obj.revision == basic_obj.revision assert obj.revision == basic_obj.revision
assert obj.revisions_list == [ original_revision, basic_obj.revision ]
assert obj.value == basic_obj.value assert obj.value == basic_obj.value
self.database.load( basic_obj.object_id, self.scheduler.thread, revision = original_revision ) 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 revised.object_id == basic_obj.object_id
assert obj.revision == original_revision assert revised.value == 1
assert obj.value == 1 assert revised.revision == original_revision
assert id( obj.revisions_list ) != id( revised.revisions_list )
assert revised.revisions_list == [ original_revision ]
g = gen() g = gen()
self.scheduler.add( g ) self.scheduler.add( g )
@ -171,6 +201,7 @@ class Test_database( object ):
def test_reload( self ): def test_reload( self ):
def gen(): def gen():
basic_obj = Some_object( object_id = "5", value = 1 ) basic_obj = Some_object( object_id = "5", value = 1 )
original_revision = basic_obj.revision
self.database.save( basic_obj, self.scheduler.thread ) self.database.save( basic_obj, self.scheduler.thread )
yield Scheduler.SLEEP yield Scheduler.SLEEP
@ -192,6 +223,8 @@ class Test_database( object ):
assert obj.object_id == basic_obj.object_id assert obj.object_id == basic_obj.object_id
assert obj.value == 55 assert obj.value == 55
assert obj.revision == original_revision
assert obj.revisions_list == [ original_revision ]
g = gen() g = gen()
self.scheduler.add( g ) self.scheduler.add( g )
@ -229,6 +262,7 @@ class Test_database( object ):
assert obj.object_id == basic_obj.object_id assert obj.object_id == basic_obj.object_id
assert obj.revision == original_revision assert obj.revision == original_revision
assert obj.revisions_list == [ original_revision ]
assert obj.value == 55 assert obj.value == 55
g = gen() g = gen()

View File

@ -1,5 +1,6 @@
import cherrypy import cherrypy
import cgi import cgi
from urllib import quote
from Test_controller import Test_controller from Test_controller import Test_controller
from controller.Scheduler import Scheduler from controller.Scheduler import Scheduler
from model.Notebook import Notebook 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"notebook_id" ) == self.notebook.object_id
assert result.get( u"note_id" ) == self.note.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 ): def test_contents( self ):
self.login() self.login()
@ -100,6 +112,28 @@ class Test_notebooks( Test_controller ):
assert note.object_id == self.note.object_id 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&note_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 ): def test_contents_without_login( self ):
result = self.http_get( result = self.http_get(
"/notebooks/contents?notebook_id=%s" % self.notebook.object_id, "/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.title == self.note.title
assert note.contents == self.note.contents 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"<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,
), 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 ): def test_load_note_without_login( self ):
result = self.http_post( "/notebooks/load_note/", dict( result = self.http_post( "/notebooks/load_note/", dict(
notebook_id = self.notebook.object_id, notebook_id = self.notebook.object_id,
@ -510,6 +573,7 @@ class Test_notebooks( Test_controller ):
note_id = self.note.object_id, note_id = self.note.object_id,
), session_id = self.session_id ) ), session_id = self.session_id )
print result
assert result.get( "note" ) == None assert result.get( "note" ) == None
def test_delete_note_without_login( self ): def test_delete_note_without_login( self ):

View File

@ -24,10 +24,11 @@ class Note( Persistent ):
self.__title = None self.__title = None
self.__contents = None or "" self.__contents = None or ""
self.__set_contents( contents ) self.__set_contents( contents, new_revision = False )
def __set_contents( self, contents ): def __set_contents( self, contents, new_revision = True ):
self.update_revision() if new_revision:
self.update_revision()
self.__contents = contents self.__contents = contents
# parse title out of the beginning of the contents # parse title out of the beginning of the contents

View File

@ -10,7 +10,9 @@ class Persistent( object ):
def update_revision( self ): def update_revision( self ):
self.__revision = datetime.now() 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 ): def revision_id( self ):
return "%s %s" % ( self.__object_id, self.__revision ) return "%s %s" % ( self.__object_id, self.__revision )

View File

@ -27,6 +27,10 @@ h3 {
-webkit-border-radius: 0.5em; -webkit-border-radius: 0.5em;
} }
.small_text {
font-size: 0.75em;
}
ul li { ul li {
margin-top: 0.5em; margin-top: 0.5em;
} }

View File

@ -33,5 +33,5 @@ A single search looks through every word in the entire wiki.</li>
</ul> </ul>
<p> <p>
Sound interesting? Then <a href="/notes/new">take a tour</a> or <a href="/notes/new">try it out</a> for yourself! Sound interesting? Then <a href="/notebooks/%s?note_id=new">take a tour</a> or <a href="/notebooks/%s?note_id=new">try it out</a> for yourself!
</p> </p>

View File

@ -1,6 +1,6 @@
<h3>login</h3> <h3>login</h3>
No account yet? Want to make a wiki? You can <a href="/notes/new">try it out</a> for free. No account yet? Want to make a wiki? You can <a href="/notebooks/%s?note_id=new">try it out</a> for free.
<form id="login_form"> <form id="login_form">
<p> <p>
@ -18,6 +18,6 @@ No account yet? Want to make a wiki? You can <a href="/notes/new">try it out</a>
</p> </p>
<p> <p>
Forgot? Need your <a href="/notes/new">password reset</a>? Forgot? Need your <a href="/notebooks/%s?note_id=new">password reset</a>?
</p> </p>
</form> </form>

View File

@ -1,5 +1,5 @@
<a href="/notes/new">about</a> - <a href="/notebooks/%s?note_id=new">about</a> -
<a href="/notes/new">features</a> - <a href="/notebooks/%s?note_id=new">features</a> -
<a href="/notes/new">take a tour</a> - <a href="/notebooks/%s?note_id=new">take a tour</a> -
<a href="/notes/new">try it out</a> - <a href="/notebooks/%s?note_id=new">try it out</a> -
<a href="/notes/new">login</a> <a href="/notebooks/%s?note_id=new">login</a>

View File

@ -1,7 +1,7 @@
<h3>supported browsers</h3> <h3>supported browsers</h3>
<p> <p>
Luminotes makes use of some <a href="/notes/new">advanced browser features</a>, Luminotes makes use of some <a href="/notebooks/%s?note_id=new">advanced browser features</a>,
so not all browsers will work for editing your wiki. Supported browsers include: so not all browsers will work for editing your wiki. Supported browsers include:
</p> </p>

View File

@ -35,5 +35,5 @@ dream of giving out your email address.
<input type="submit" name="signup_button" id="signup_button" class="button" value="sign up" /> <input type="submit" name="signup_button" id="signup_button" class="button" value="sign up" />
</p> </p>
Please make sure you're using one of the <a href="/notes/new">supported browsers</a>. Please make sure you're using one of the <a href="/notebooks/?note_id=new">supported browsers</a>.
</form> </form>

View File

@ -126,13 +126,13 @@ Editor.prototype.finish_init = function () {
if ( this.read_write ) { if ( this.read_write ) {
connect( this.document, "onkeydown", function ( event ) { self.key_pressed( event ); } ); connect( this.document, "onkeydown", function ( event ) { self.key_pressed( event ); } );
connect( this.document, "onkeyup", function ( event ) { self.key_released( 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, "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 // special-case: connect any submit buttons within the contents of this note
var signup_button = withDocument( this.document, function () { return getElement( "signup_button" ); } ); 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 // return true if the specified state is enabled
Editor.prototype.state_enabled = function ( state_name ) { Editor.prototype.state_enabled = function ( state_name ) {
if ( !this.read_write ) return false;
state_name = state_name.toLowerCase(); state_name = state_name.toLowerCase();
var format_block = this.document.queryCommandValue( "formatblock" ).toLowerCase(); var format_block = this.document.queryCommandValue( "formatblock" ).toLowerCase();
var heading = ( format_block == "h3" || format_block == "heading 3" ); var heading = ( format_block == "h3" || format_block == "heading 3" );

View File

@ -17,7 +17,8 @@ function Wiki() {
this.invoker.invoke( this.invoker.invoke(
"/notebooks/contents", "GET", { "/notebooks/contents", "GET", {
"notebook_id": this.notebook_id, "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 ); } 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 // don't actually create an editor if a particular note was provided in the result
if ( !result.note ) { if ( !result.note ) {
var focus = ( i == 0 ); 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 // 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 ) 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 ) { 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; var self = this;
this.invoker.invoke( this.invoker.invoke(
"/notebooks/load_note", "GET", { "/notebooks/load_note", "GET", {
"notebook_id": this.notebook_id, "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 ) { 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 note_text = result.note.contents;
var revisions_list = result.note.revisions_list; var revisions_list = result.note.revisions_list;
} else { } else {
@ -198,10 +203,15 @@ Wiki.prototype.parse_loaded_editor = function ( result, insert_after_iframe_id,
var revisions_list = new Array(); 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_messages();
this.clear_pulldowns(); 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 = "<p class=\"small_text\">Previous revision from " + short_revision + "</p>" + note_text;
}
var startup = this.startup_notes[ id ]; 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 ) { if ( this.read_write ) {
connect( editor, "state_changed", this, "editor_state_changed" ); 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_messages();
this.clear_pulldowns(); this.clear_pulldowns();
if ( this.focused_editor ) { if ( this.focused_editor && this.focused_editor.read_write ) {
this.focused_editor.focus(); this.focused_editor.focus();
this.focused_editor.exec_command( state_name || button_id ); this.focused_editor.exec_command( state_name || button_id );
this.focused_editor.resize(); this.focused_editor.resize();
@ -368,7 +384,7 @@ Wiki.prototype.toggle_link_button = function ( event ) {
this.clear_messages(); this.clear_messages();
this.clear_pulldowns(); this.clear_pulldowns();
if ( this.focused_editor ) { if ( this.focused_editor && this.focused_editor.read_write ) {
this.focused_editor.focus(); this.focused_editor.focus();
toggleElementClass( "button_down", "createLink" ); toggleElementClass( "button_down", "createLink" );
if ( hasElementClass( "createLink", "button_down" ) ) if ( hasElementClass( "createLink", "button_down" ) )
@ -413,7 +429,7 @@ Wiki.prototype.delete_editor = function ( event, editor ) {
if ( this.startup_notes[ editor.id ] ) if ( this.startup_notes[ editor.id ] )
delete 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", { this.invoker.invoke( "/notebooks/delete_note", "POST", {
"notebook_id": this.notebook_id, "notebook_id": this.notebook_id,
"note_id": editor.id "note_id": editor.id
@ -434,8 +450,7 @@ Wiki.prototype.save_editor = function ( editor, fire_and_forget ) {
editor = this.focused_editor; editor = this.focused_editor;
var self = this; var self = this;
if ( editor && !editor.empty() ) { if ( editor && editor.read_write && !editor.empty() ) {
// TODO: do something with the result other than just ignoring it
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,
@ -494,7 +509,7 @@ Wiki.prototype.display_search_results = function ( result ) {
continue; 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 ) { Wiki.prototype.toggle_editor_changes = function ( event, editor ) {
// if the pulldown is already open, then just close it // if the pulldown is already open, then just close it
var pulldown_id = "changes_" + editor.id; var pulldown_id = "changes_" + editor.id;
@ -539,7 +558,7 @@ Wiki.prototype.toggle_editor_changes = function ( event, editor ) {
return; return;
} }
new Changes_pulldown( this.notebook_id, this.invoker, editor ); new Changes_pulldown( this, this.notebook_id, this.invoker, editor );
event.stop(); event.stop();
} }
@ -552,14 +571,15 @@ Wiki.prototype.toggle_editor_options = function ( event, editor ) {
return; return;
} }
new Options_pulldown( this.notebook_id, this.invoker, editor ); new Options_pulldown( this, this.notebook_id, this.invoker, editor );
event.stop(); event.stop();
} }
connect( window, "onload", function ( event ) { new Wiki(); } ); 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.notebook_id = notebook_id;
this.div = createDOM( "div", { "id": pulldown_id, "class": "pulldown" } ); this.div = createDOM( "div", { "id": pulldown_id, "class": "pulldown" } );
this.div.pulldown = this; this.div.pulldown = this;
@ -584,8 +604,8 @@ Pulldown.prototype.shutdown = function () {
} }
function Options_pulldown( notebook_id, invoker, editor ) { function Options_pulldown( wiki, notebook_id, invoker, editor ) {
Pulldown.call( this, notebook_id, "options_" + editor.id, editor.options_button ); Pulldown.call( this, wiki, notebook_id, "options_" + editor.id, editor.options_button );
this.invoker = invoker; this.invoker = invoker;
this.editor = editor; 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.startup_checkbox.checked = this.startup_checkbox.checked ? false : true;
this.editor.startup = this.startup_checkbox.checked; this.editor.startup = this.startup_checkbox.checked;
// if this note isn't empty, save it along with its startup status // save this note along with its toggled startup state
if ( !this.editor.empty() ) { this.wiki.save_editor( this.editor );
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
} );
}
} }
Options_pulldown.prototype.shutdown = function () { Options_pulldown.prototype.shutdown = function () {
Pulldown.prototype.shutdown.call( this ); Pulldown.prototype.shutdown.call( this );
disconnectAll( this.startup_checkbox );
disconnectAll( this.startup_toggle ); disconnectAll( this.startup_toggle );
} }
function Changes_pulldown( notebook_id, invoker, editor ) { function Changes_pulldown( wiki, notebook_id, invoker, editor ) {
Pulldown.call( this, notebook_id, "changes_" + editor.id, editor.changes_button ); Pulldown.call( this, wiki, notebook_id, "changes_" + editor.id, editor.changes_button );
this.invoker = invoker; this.invoker = invoker;
this.editor = editor; this.editor = editor;
this.links = new Array();
// display list of revision timestamps in reverse chronological order // 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." ) ); appendChildNodes( this.div, createDOM( "span", "This note has no previous changes." ) );
return; return;
} }
@ -644,18 +659,35 @@ function Changes_pulldown( notebook_id, invoker, editor ) {
var revisions_list = clone( this.editor.revisions_list ); var revisions_list = clone( this.editor.revisions_list );
revisions_list.reverse(); revisions_list.reverse();
var self = this;
for ( var i = 0; i < revisions_list.length; ++i ) { for ( var i = 0; i < revisions_list.length; ++i ) {
var revision = revisions_list[ 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( var href = "/notebooks/" + this.notebook_id + "?" + queryString(
[ "note_id", "revision" ], [ "note_id", "revision" ],
[ this.editor.id, revision ] [ this.editor.id, revision ]
); );
// appendChildNodes( this.div, createDOM( "a", { "href": href, "class": "pulldown_link" }, revision ) ); var link = createDOM( "a", { "href": href, "class": "pulldown_link" }, short_revision );
appendChildNodes( this.div, createDOM( "span", {}, 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" ) ); appendChildNodes( this.div, createDOM( "br" ) );
} }
} }
Changes_pulldown.prototype = Pulldown; Changes_pulldown.prototype = Pulldown;
Changes_pulldown.prototype.constructor = Changes_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 ] );
}

View File

@ -52,7 +52,7 @@ class Initializer( object ):
for ( filename, startup ) in self.ENTRY_FILES: for ( filename, startup ) in self.ENTRY_FILES:
full_filename = os.path.join( self.HTML_PATH, filename ) 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 ) self.database.next_id( self.scheduler.thread )
note_id = ( yield Scheduler.SLEEP ) note_id = ( yield Scheduler.SLEEP )

View File

@ -34,7 +34,7 @@ class Initializer( object ):
self.scheduler.wait_for( thread ) self.scheduler.wait_for( thread )
def update_main_notebook( self ): 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 ) anonymous = ( yield Scheduler.SLEEP )
main_notebook = anonymous.notebooks[ 0 ]._Read_only_notebook__wrapped main_notebook = anonymous.notebooks[ 0 ]._Read_only_notebook__wrapped
startup_notes = [] startup_notes = []
@ -42,7 +42,7 @@ class Initializer( object ):
# update all of the notes in the main notebook # update all of the notes in the main notebook
for ( filename, startup ) in self.ENTRY_FILES: for ( filename, startup ) in self.ENTRY_FILES:
full_filename = os.path.join( self.HTML_PATH, filename ) 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"" ) title = filename.replace( u".html", u"" )
note = main_notebook.lookup_note_by_title( title ) note = main_notebook.lookup_note_by_title( title )

View File

@ -6,7 +6,7 @@ from Toolbar import Toolbar
class Main_page( Page ): 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 title = None
Page.__init__( Page.__init__(
@ -14,6 +14,7 @@ class Main_page( Page ):
title, title,
Input( type = u"hidden", name = u"notebook_id", id = u"notebook_id", value = notebook_id or "" ), 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"note_id", id = u"note_id", value = note_id or "" ),
Input( type = u"hidden", name = u"revision", id = u"revision", value = revision or "" ),
Div( Div(
id = u"status_area", id = u"status_area",
), ),