From 1d0867d77657f588b600a011989c6267f5069271 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Tue, 29 Apr 2008 00:54:08 +0000 Subject: [PATCH] * Renamed existing controller.Notebooks.load_recent_notes method to just recent_notes(). * Implemented new controller.Notebooks.load_recent_updates() method. * Added new Wiki.js total_notes_count_updated signal * Added "more" and "less" links to "recent updates" table. * Updated Wiki.js Recent_notes to support new "more" and "less" links. * Commented out unfinished discussion forums unit test. --- NEWS | 3 + controller/Notebooks.py | 52 ++++++++++++-- controller/Root.py | 2 +- controller/test/Test_forums.py | 2 +- controller/test/Test_notebooks.py | 10 +-- static/js/Wiki.js | 109 ++++++++++++++++++++++++++++-- view/Note_tree_area.py | 15 +++- 7 files changed, 172 insertions(+), 21 deletions(-) diff --git a/NEWS b/NEWS index 6d15052..50470fd 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,6 @@ +1.3.6: April ??, 2008 + * Can now click "more" link to display more than ten "recent updates". + 1.3.5: April 24, 2008 * Reducing the number of links in the header by consolidating several into one "support" link. diff --git a/controller/Notebooks.py b/controller/Notebooks.py index 5d69e61..ced345a 100644 --- a/controller/Notebooks.py +++ b/controller/Notebooks.py @@ -3,7 +3,7 @@ import cgi import cherrypy from datetime import datetime from Expose import expose -from Validate import validate, Valid_string, Validation_error, Valid_bool +from Validate import validate, Valid_string, Validation_error, Valid_bool, Valid_int from Database import Valid_id, Valid_revision, end_transaction from Users import grab_user_id, Access_error from Expire import strongly_expire @@ -1364,19 +1364,61 @@ class Notebooks( object ): return dict() - def load_recent_notes( self, notebook_id, start = 0, count = 10, user_id = None ): + @expose( view = Json ) + @strongly_expire + @end_transaction + @grab_user_id + @validate( + notebook_id = Valid_id(), + start = Valid_int( min = 0 ), + count = Valid_int( min = 1 ), + user_id = Valid_id( none_okay = True ), + ) + def load_recent_updates( self, notebook_id, start, count, user_id = None ): """ - Provide the information necessary to display the page for a particular notebook's most recent - notes. + Provide the information necessary to display a notebook's recent updated/created notes, in + reverse chronological order by update time. + @type notebook_id: unicode - @param notebook_id: id of the notebook to display + @param notebook_id: id of the notebook containing the notes @type start: unicode or NoneType @param start: index of recent note to start with (defaults to 0, the most recent note) @type count: int or NoneType @param count: number of recent notes to display (defaults to 10 notes) @type user_id: unicode or NoneType @param user_id: id of current logged-in user (if any) + @rtype: json dict + @return: { 'notes': recent_notes_list } + @raise Access_error: the current user doesn't have access to the given notebook or note + """ + if not self.__users.check_access( user_id, notebook_id ): + raise Access_error() + + notebook = self.__database.load( Notebook, notebook_id ) + + if notebook is None: + raise Access_error() + + recent_notes = self.__database.select_many( Note, notebook.sql_load_notes( start = start, count = count ) ) + + return dict( + notes = recent_notes, + ) + + def recent_notes( self, notebook_id, start = 0, count = 10, user_id = None ): + """ + Return the given notebook's recently updated notes in reverse chronological order by creation + time. + + @type notebook_id: unicode + @param notebook_id: id of the notebook containing the notes + @type start: unicode or NoneType + @param start: index of recent note to start with (defaults to 0, the most recent note) + @type count: int or NoneType + @param count: number of recent notes to return (defaults to 10 notes) + @type user_id: unicode or NoneType + @param user_id: id of current logged-in user (if any) @rtype: dict @return: data for Main_page() constructor @raise Access_error: the current user doesn't have access to the given notebook or note diff --git a/controller/Root.py b/controller/Root.py index cd22ee7..b2dbc47 100644 --- a/controller/Root.py +++ b/controller/Root.py @@ -230,7 +230,7 @@ class Root( object ): result = self.__users.current( user_id ) blog_notebooks = [ nb for nb in result[ "notebooks" ] if nb.name == u"Luminotes blog" ] - result.update( self.__notebooks.load_recent_notes( blog_notebooks[ 0 ].object_id, start, count, user_id ) ) + result.update( self.__notebooks.recent_notes( blog_notebooks[ 0 ].object_id, start, count, user_id ) ) # if a single note was requested, just return that one note if note_id: diff --git a/controller/test/Test_forums.py b/controller/test/Test_forums.py index ec9a4a0..4234d15 100644 --- a/controller/test/Test_forums.py +++ b/controller/test/Test_forums.py @@ -40,7 +40,7 @@ class Test_forums( Test_controller ): self.database.save( self.anonymous ) self.database.execute( self.anonymous.sql_save_notebook( self.anon_notebook.object_id ) ) - def test_index( self ): + def XXXtest_index( self ): # TODO result = self.http_get( "/forums/" ) assert result diff --git a/controller/test/Test_notebooks.py b/controller/test/Test_notebooks.py index 1a5e7ec..93e7511 100644 --- a/controller/test/Test_notebooks.py +++ b/controller/test/Test_notebooks.py @@ -3210,7 +3210,7 @@ class Test_notebooks( Test_controller ): assert u"access" in result[ u"error" ] def test_recent_notes( self ): - result = cherrypy.root.notebooks.load_recent_notes( + result = cherrypy.root.notebooks.recent_notes( self.notebook.object_id, user_id = self.user.object_id, ) @@ -3239,7 +3239,7 @@ class Test_notebooks( Test_controller ): assert user.storage_bytes == 0 def test_recent_notes_with_start( self ): - result = cherrypy.root.notebooks.load_recent_notes( + result = cherrypy.root.notebooks.recent_notes( self.notebook.object_id, start = 1, user_id = self.user.object_id, @@ -3268,7 +3268,7 @@ class Test_notebooks( Test_controller ): assert user.storage_bytes == 0 def test_recent_notes_with_count( self ): - result = cherrypy.root.notebooks.load_recent_notes( + result = cherrypy.root.notebooks.recent_notes( self.notebook.object_id, count = 1, user_id = self.user.object_id, @@ -3298,14 +3298,14 @@ class Test_notebooks( Test_controller ): @raises( Access_error ) def test_recent_notes_with_unknown_notebok( self ): - result = cherrypy.root.notebooks.load_recent_notes( + result = cherrypy.root.notebooks.recent_notes( self.unknown_notebook_id, user_id = self.user.object_id, ) @raises( Access_error ) def test_recent_notes_with_incorrect_user( self ): - result = cherrypy.root.notebooks.load_recent_notes( + result = cherrypy.root.notebooks.recent_notes( self.notebook.object_id, user_id = self.anonymous.object_id, ) diff --git a/static/js/Wiki.js b/static/js/Wiki.js index a720ab6..db8ca7e 100644 --- a/static/js/Wiki.js +++ b/static/js/Wiki.js @@ -18,8 +18,6 @@ function Wiki( invoker ) { this.after_login = getElement( "after_login" ).value; this.signup_plan = getElement( "signup_plan" ).value; this.font_size = null; - this.note_tree = new Note_tree( this, this.notebook_id, this.invoker ); - this.recent_notes = new Recent_notes( this, this.notebook_id ); var total_notes_count_node = getElement( "total_notes_count" ); if ( total_notes_count_node ) @@ -27,6 +25,9 @@ function Wiki( invoker ) { else this.total_notes_count = null; + this.note_tree = new Note_tree( this, this.notebook_id, this.invoker ); + this.recent_notes = new Recent_notes( this, this.notebook_id, this.invoker ); + // grab the current notebook from the list of available notebooks this.notebooks = evalJSON( getElement( "notebooks" ).value ); for ( var i in this.notebooks ) { @@ -1841,18 +1842,21 @@ Wiki.prototype.increment_total_notes_count = function () { if ( this.total_notes_count == null ) return; this.total_notes_count += 1; replaceChildNodes( "total_notes_count", this.total_notes_count ); + signal( this, "total_notes_count_updated", this.total_notes_count ); } Wiki.prototype.decrement_total_notes_count = function () { if ( this.total_notes_count == null ) return; this.total_notes_count -= 1; replaceChildNodes( "total_notes_count", this.total_notes_count ); + signal( this, "total_notes_count_updated", this.total_notes_count ); } Wiki.prototype.zero_total_notes_count = function () { if ( this.total_notes_count == null ) return; this.total_notes_count = 0; replaceChildNodes( "total_notes_count", this.total_notes_count ); + signal( this, "total_notes_count_updated", this.total_notes_count ); } Wiki.prototype.start_notebook_rename = function () { @@ -2889,9 +2893,15 @@ Note_tree.prototype.display_child_links = function ( result, link, children_area connect_expander( expander, note_id ); } -function Recent_notes( wiki, notebook_id ) { +function Recent_notes( wiki, notebook_id, invoker ) { this.wiki = wiki; this.notebook_id = notebook_id; + this.invoker = invoker; + + this.INCREMENT = 10; + this.max_recent_notes_count = this.INCREMENT; // maximum increases when the user clicks "more" + this.total_notes_count = 0; + this.total_notes_count_updated( wiki.total_notes_count ); // if there's no recent notes table, there's nothing to do with recent notes! if ( !getElement( "recent_notes_table" ) ) @@ -2913,6 +2923,33 @@ function Recent_notes( wiki, notebook_id ) { connect( wiki, "note_added", function ( editor ) { self.add_link( editor ); } ); connect( wiki, "note_removed", function ( id ) { self.remove_link( id ); } ); connect( wiki, "note_saved", function ( editor ) { self.update_link( editor ); } ); + connect( wiki, "total_notes_count_updated", function ( count ) { self.total_notes_count_updated( count ); } ); + + // connect to the "more" navigation link + connect( "recent_notes_more_link", "onclick", function ( event ) { self.more_clicked( event ); } ); + connect( "recent_notes_less_link", "onclick", function ( event ) { self.less_clicked( event ); } ); +} + +Recent_notes.prototype.links_count = function () { + var recent_links = getElementsByTagAndClassName( "a", "recent_note_link", "note_tree_area" ); + return recent_links.length; +} + +Recent_notes.prototype.total_notes_count_updated = function( count ) { + this.total_notes_count = count; + this.update_navigation_links(); +} + +Recent_notes.prototype.update_navigation_links = function() { + if ( this.total_notes_count > this.max_recent_notes_count ) + removeElementClass( "recent_notes_more_link", "undisplayed" ); + else + addElementClass( "recent_notes_more_link", "undisplayed" ); + + if ( this.max_recent_notes_count > this.INCREMENT ) + removeElementClass( "recent_notes_less_link", "undisplayed" ); + else + addElementClass( "recent_notes_less_link", "undisplayed" ); } Recent_notes.prototype.link_clicked = function ( event ) { @@ -2928,16 +2965,76 @@ Recent_notes.prototype.link_clicked = function ( event ) { event.stop(); } +Recent_notes.prototype.more_clicked = function ( event ) { + event.stop(); + this.max_recent_notes_count += this.INCREMENT; + + var self = this; + var links_count = this.links_count(); + + this.invoker.invoke( + "/notebooks/load_recent_updates", "GET", { + "notebook_id": this.notebook_id, + "start": links_count, + "count": this.max_recent_notes_count - links_count + }, + function ( result ) { self.append_links( result ); } + ); +} + +Recent_notes.prototype.append_links = function ( result ) { + var self = this; + var table = getElement( "recent_notes_table" ); + var links_count = this.links_count(); + + for ( var i in result.notes ) { + var note = result.notes[ i ]; + var row = table.insertRow( links_count + 1 ); + row.setAttribute( "id", "recent_note_item_" + note.object_id ); + addElementClass( row, "recent_note_item" ); + + var expander_td = row.insertCell( 0 ); + expander_td.innerHTML = '
' + + ( note.title || 'untitled note' ) + ''; + + connect( "recent_note_link_" + note.object_id, "onclick", function ( event ) { self.link_clicked( event ); } ); + + links_count += 1; + } + + this.update_navigation_links(); +} + +Recent_notes.prototype.less_clicked = function ( event ) { + event.stop(); + this.max_recent_notes_count -= this.INCREMENT; + + var rows_to_remove_count = this.links_count() - this.max_recent_notes_count; + if ( rows_to_remove_count <= 0 ) + return; + + var rows = getElementsByTagAndClassName( "tr", "recent_note_item", "recent_notes_table" ); + var row_count = rows.length; + + for ( var i = 0; i < rows_to_remove_count; ++i ) { + removeElement( rows[ row_count - i - 1 ] ); + } + + this.update_navigation_links(); +} + Recent_notes.prototype.add_link = function ( editor ) { // if the link is already present in the recent notes list, bail var item = getElement( "recent_note_item_" + editor.id ) if ( item ) return; - MAX_RECENT_NOTES_COUNT = 10; - // if there will be too many recent notes listed once another is added, then remove the last one var recent_items = getElementsByTagAndClassName( "tr", "recent_note_item", "recent_notes_table" ); - if ( recent_items && recent_items.length >= MAX_RECENT_NOTES_COUNT ) { + if ( recent_items && recent_items.length >= this.max_recent_notes_count ) { var last_item = recent_items[ recent_items.length - 1 ]; removeElement( last_item ); } diff --git a/view/Note_tree_area.py b/view/Note_tree_area.py index 42e23a3..a201a07 100644 --- a/view/Note_tree_area.py +++ b/view/Note_tree_area.py @@ -46,6 +46,13 @@ class Note_tree_area( Div ): root_note_id = note.object_id, base_name = u"recent_note", ) for note in recent_notes ], + navigation = Tbody( Tr( + Td(), + Td( + A( u"more", href = u"#", id = u"recent_notes_more_link", class_ = u"undisplayed" ), + A( u"less", href = u"#", id = u"recent_notes_less_link", class_ = u"undisplayed" ), + ), + ), id = u"recent_notes_navigation" ), tree_id = "recent_notes_table", ), ) or None, @@ -80,13 +87,15 @@ class Note_tree_area( Div ): ) @staticmethod - def make_tree( items, other_node = None, tree_id = None ): + def make_tree( first_node, second_node = None, third_node = None, navigation = None, tree_id = None ): return Table( Tbody( - items, - other_node, + first_node, + second_node, + third_node, id = tree_id and tree_id + "_body" or None, ), + navigation, id = tree_id or None, class_ = u"note_tree_table", )