diff --git a/controller/Notebooks.py b/controller/Notebooks.py index da98f17..5d429d3 100644 --- a/controller/Notebooks.py +++ b/controller/Notebooks.py @@ -474,7 +474,8 @@ class Notebooks( object ): def delete_note( self, notebook_id, note_id, user_id ): """ Delete the given note from its notebook and move it to the notebook's trash. The note is added - as a startup note within the trash. + as a startup note within the trash. If the given notebook is the trash and the given note is + already there, then it is deleted from the trash forever. @type notebook_id: unicode @param notebook_id: id of notebook the note is in @@ -571,6 +572,51 @@ class Notebooks( object ): yield dict() + @expose( view = Json ) + @wait_for_update + @grab_user_id + @async + @update_client + @validate( + notebook_id = Valid_id(), + user_id = Valid_id( none_okay = True ), + ) + def delete_all_notes( self, notebook_id, user_id ): + """ + Delete all notes from the given notebook and move them to the notebook's trash (if any). The + notes are added as startup notes within the trash. If the given notebook is the trash, then + all notes in the trash are deleted forever. + + @type notebook_id: unicode + @param notebook_id: id of notebook the note is in + @type user_id: unicode or NoneType + @param user_id: id of current logged-in user (if any), determined by @grab_user_id + @rtype: json dict + @return: {} + @raise Access_error: the current user doesn't have access to the given notebook + @raise Validation_error: one of the arguments is invalid + """ + 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 not notebook: + raise Access_error() + + for note in notebook.notes: + notebook.remove_note( note ) + + if notebook.trash: + note.deleted_from = notebook.object_id + notebook.trash.add_note( note ) + notebook.trash.add_startup_note( note ) + + self.__database.save( notebook ) + + yield dict() @expose( view = Note_page ) @validate( id = Valid_string( min = 1, max = 100 ) ) def blank_note( self, id ): diff --git a/controller/test/Test_notebooks.py b/controller/test/Test_notebooks.py index 712608e..df17fd8 100644 --- a/controller/test/Test_notebooks.py +++ b/controller/test/Test_notebooks.py @@ -917,6 +917,29 @@ class Test_notebooks( Test_controller ): assert note.object_id == self.note.object_id assert note.deleted_from == self.notebook.object_id + def test_delete_note_from_trash( self ): + self.login() + + # first, delete the note from the main notebook, thereby moving it to the trash + result = self.http_post( "/notebooks/delete_note/", dict( + notebook_id = self.notebook.object_id, + note_id = self.note.object_id, + ), session_id = self.session_id ) + + # then, delete the note from the trash + result = self.http_post( "/notebooks/delete_note/", dict( + notebook_id = self.notebook.trash.object_id, + note_id = self.note.object_id, + ), session_id = self.session_id ) + + # test that the deleted note is actually deleted from 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_delete_note_without_login( self ): result = self.http_post( "/notebooks/delete_note/", dict( notebook_id = self.notebook.object_id, @@ -1119,6 +1142,108 @@ class Test_notebooks( Test_controller ): assert note.object_id == self.note.object_id assert note.deleted_from == None + def test_delete_all_notes( self ): + self.login() + + result = self.http_post( "/notebooks/delete_all_notes/", dict( + notebook_id = self.notebook.object_id, + ), session_id = self.session_id ) + + # test that all notes are actually deleted + 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 ) + + assert result.get( "note" ) == None + + result = self.http_post( "/notebooks/load_note/", dict( + notebook_id = self.notebook.object_id, + note_id = self.note2.object_id, + ), session_id = self.session_id ) + + assert result.get( "note" ) == None + + # test that the notes get moved to 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 ) + + note = result.get( "note" ) + assert note + assert note.object_id == self.note.object_id + assert note.deleted_from == self.notebook.object_id + + result = self.http_post( "/notebooks/load_note/", dict( + notebook_id = self.notebook.trash.object_id, + note_id = self.note2.object_id, + ), session_id = self.session_id ) + + note2 = result.get( "note" ) + assert note2 + assert note2.object_id == self.note2.object_id + assert note2.deleted_from == self.notebook.object_id + + def test_delete_all_notes_from_trash( self ): + self.login() + + # first, delete all notes from the main notebook, thereby moving them to the trash + result = self.http_post( "/notebooks/delete_all_notes/", dict( + notebook_id = self.notebook.object_id, + ), session_id = self.session_id ) + + # then, delete all notes from the trash + result = self.http_post( "/notebooks/delete_all_notes/", dict( + notebook_id = self.notebook.trash.object_id, + ), session_id = self.session_id ) + + # test that all notes are actually deleted from 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 + + result = self.http_post( "/notebooks/load_note/", dict( + notebook_id = self.notebook.trash.object_id, + note_id = self.note2.object_id, + ), session_id = self.session_id ) + + assert result.get( "note" ) == None + + def test_delete_all_notes_without_login( self ): + result = self.http_post( "/notebooks/delete_all_notes/", dict( + notebook_id = self.notebook.object_id, + ), session_id = self.session_id ) + + assert result.get( "error" ) + + def test_delete_all_notes_with_unknown_notebook( self ): + self.login() + + result = self.http_post( "/notebooks/delete_all_notes/", dict( + notebook_id = self.unknown_notebook_id, + ), session_id = self.session_id ) + + # test that the notes haven't been deleted + 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 + + result = self.http_post( "/notebooks/load_note/", dict( + notebook_id = self.notebook.object_id, + note_id = self.note2.object_id, + ), session_id = self.session_id ) + + note2 = result.get( "note" ) + assert note2.object_id == self.note2.object_id + def test_blank_note( self ): result = self.http_get( "/notebooks/blank_note/5" ) assert result[ u"id" ] == u"5" diff --git a/static/js/Wiki.js b/static/js/Wiki.js index 6d417ad..ba28b98 100644 --- a/static/js/Wiki.js +++ b/static/js/Wiki.js @@ -102,7 +102,13 @@ Wiki.prototype.populate = function ( result ) { var header_area = getElement( "notebook_header_area" ); replaceChildNodes( header_area, createDOM( "b", {}, this.notebook.name ) ); + if ( this.parent_id ) { + appendChildNodes( header_area, createDOM( "span", {}, ": " ) ); + var empty_trash_link = createDOM( "a", { "href": location.href }, "empty trash" ); + appendChildNodes( header_area, empty_trash_link ); + connect( empty_trash_link, "onclick", function ( event ) { try{ self.delete_all_editors( event ); } catch(e){ alert(e); } } ); + appendChildNodes( header_area, createDOM( "span", {}, " | " ) ); appendChildNodes( header_area, createDOM( "a", { "href": "/notebooks/" + this.parent_id }, "return to notebook" ) ); } @@ -198,7 +204,7 @@ Wiki.prototype.populate = function ( result ) { this.create_editor( result.note.object_id, result.note.contents, result.note.deleted_from, result.note.revisions_list, undefined, read_write, false, true ); if ( !this.notebook.trash && result.startup_notes.length == 0 && !result.note ) - this.display_message( "There are no notes here." ) + this.display_message( "The trash is empty." ) } Wiki.prototype.create_blank_editor = function ( event ) { @@ -611,6 +617,7 @@ Wiki.prototype.delete_editor = function ( event, editor ) { if ( this.startup_notes[ editor.id ] ) delete this.startup_notes[ editor.id ]; + // FIXME: does saving and deleting cause a race in which the "deleted" note is then saved right back where it was in the notebook? this.save_editor( editor, true ); if ( this.read_write && editor.read_write ) { @@ -880,6 +887,31 @@ Wiki.prototype.clear_pulldowns = function () { } } +Wiki.prototype.delete_all_editors = function ( event ) { + this.clear_messages(); + this.clear_pulldowns(); + + this.startup_notes = new Array(); + + if ( this.read_write ) { + this.invoker.invoke( "/notebooks/delete_all_notes", "POST", { + "notebook_id": this.notebook_id + } ); + } + + this.focused_editor = null; + + var iframes = getElementsByTagAndClassName( "iframe", "note_frame" ); + for ( var i in iframes ) { + var editor = iframes[ i ].editor; + editor.shutdown(); + } + + this.display_message( "The trash is empty." ); + + event.stop(); +} + Wiki.prototype.brief_revision = function ( revision ) { return revision.split( /\.\d/ )[ 0 ]; // strip off seconds from the timestamp }