diff --git a/controller/Notebooks.py b/controller/Notebooks.py index ace479d..7ad672f 100644 --- a/controller/Notebooks.py +++ b/controller/Notebooks.py @@ -835,7 +835,6 @@ class Notebooks( object ): notebook = self.__database.load( Notebook, notebook_id ) - # TODO: maybe if notebook.deleted is already True, then the notebook should be "deleted forever" if not notebook: raise Access_error() @@ -857,6 +856,47 @@ class Notebooks( object ): redirect = u"/notebooks/%s?deleted_id=%s" % ( remaining_notebook.object_id, notebook.object_id ), ) + @expose( view = Json ) + @grab_user_id + @validate( + notebook_id = Valid_id(), + user_id = Valid_id( none_okay = True ), + ) + def delete_forever( self, notebook_id, user_id ): + """ + Delete the given notebook permanently (by simply revoking the user's access to it). + + @type notebook_id: unicode + @param notebook_id: id of notebook to delete + @type user_id: unicode or NoneType + @param user_id: id of current logged-in user (if any) + @rtype 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 + """ + if user_id is None: + raise Access_error() + + user = self.__database.load( User, user_id ) + + if not self.__users.check_access( user_id, notebook_id, read_write = True ): + raise Access_error() + + notebook = self.__database.load( Notebook, notebook_id ) + + if not notebook: + raise Access_error() + + # prevent deletion of a trash notebook directly + if notebook.name == u"trash": + raise Access_error() + + self.__database.execute( user.sql_remove_notebook( notebook_id ), commit = False ) + self.__database.commit() + + return dict() + @expose( view = Json ) @grab_user_id @validate( diff --git a/controller/test/Test_controller.py b/controller/test/Test_controller.py index fa31514..b7916d3 100644 --- a/controller/test/Test_controller.py +++ b/controller/test/Test_controller.py @@ -26,6 +26,15 @@ class Test_controller( object ): User.sql_save_notebook = lambda self, notebook_id, read_write = False: \ lambda database: sql_save_notebook( self, notebook_id, read_write, database ) + def sql_remove_notebook( self, notebook_id, database ): + if self.object_id in database.user_notebook: + for access_tuple in database.user_notebook[ self.object_id ]: + if access_tuple[ 0 ] == notebook_id: + database.user_notebook[ self.object_id ].remove( access_tuple ) + + User.sql_remove_notebook = lambda self, notebook_id: \ + lambda database: sql_remove_notebook( self, notebook_id, database ) + def sql_load_notebooks( self, parents_only, undeleted_only, database ): notebooks = [] notebook_tuples = database.user_notebook.get( self.object_id ) diff --git a/controller/test/Test_notebooks.py b/controller/test/Test_notebooks.py index 8666794..568daa1 100644 --- a/controller/test/Test_notebooks.py +++ b/controller/test/Test_notebooks.py @@ -1805,7 +1805,26 @@ class Test_notebooks( Test_controller ): def test_contents_after_delete( self ): self.login() - result = self.http_post( "/notebooks/delete", dict( + self.http_post( "/notebooks/delete", dict( + notebook_id = self.notebook.object_id, + ), session_id = self.session_id ) + + result = cherrypy.root.notebooks.contents( + notebook_id = self.notebook.object_id, + user_id = self.user.object_id, + ) + + notebook = result[ "notebook" ] + assert notebook.deleted == True + + def test_contents_after_delete_twice( self ): + self.login() + + self.http_post( "/notebooks/delete", dict( + notebook_id = self.notebook.object_id, + ), session_id = self.session_id ) + + self.http_post( "/notebooks/delete", dict( notebook_id = self.notebook.object_id, ), session_id = self.session_id ) @@ -1833,6 +1852,74 @@ class Test_notebooks( Test_controller ): assert u"error" in result + def test_delete_forever( self ): + self.login() + + result = self.http_post( "/notebooks/delete_forever", dict( + notebook_id = self.notebook.object_id, + ), session_id = self.session_id ) + + assert u"error" not in result + + @raises( Access_error ) + def test_contents_after_delete_forever( self ): + self.login() + + self.http_post( "/notebooks/delete_forever", dict( + notebook_id = self.notebook.object_id, + ), session_id = self.session_id ) + + result = cherrypy.root.notebooks.contents( + notebook_id = self.notebook.object_id, + user_id = self.user.object_id, + ) + + def test_delete_then_delete_forever( self ): + self.login() + + result = self.http_post( "/notebooks/delete", dict( + notebook_id = self.notebook.object_id, + ), session_id = self.session_id ) + + result = self.http_post( "/notebooks/delete_forever", dict( + notebook_id = self.notebook.object_id, + ), session_id = self.session_id ) + + assert u"error" not in result + + @raises( Access_error ) + def test_contents_after_delete_then_delete_forever( self ): + self.login() + + self.http_post( "/notebooks/delete", dict( + notebook_id = self.notebook.object_id, + ), session_id = self.session_id ) + + self.http_post( "/notebooks/delete_forever", dict( + notebook_id = self.notebook.object_id, + ), session_id = self.session_id ) + + result = cherrypy.root.notebooks.contents( + notebook_id = self.notebook.object_id, + user_id = self.user.object_id, + ) + + def test_delete_forever_without_login( self ): + result = self.http_post( "/notebooks/delete_forever", dict( + notebook_id = self.notebook.object_id, + ), session_id = self.session_id ) + + assert result[ u"error" ] + + def test_delete_forever_trash( self ): + self.login() + + result = self.http_post( "/notebooks/delete_forever", dict( + notebook_id = self.notebook.trash_id, + ), session_id = self.session_id ) + + assert u"error" in result + def test_undelete( self ): self.login() diff --git a/model/User.py b/model/User.py index 3a91ead..36a6485 100644 --- a/model/User.py +++ b/model/User.py @@ -156,6 +156,13 @@ class User( Persistent ): "insert into user_notebook ( user_id, notebook_id, read_write ) values " + \ "( %s, %s, %s );" % ( quote( self.object_id ), quote( notebook_id ), quote( read_write and 't' or 'f' ) ) + def sql_remove_notebook( self, notebook_id ): + """ + Return a SQL string to remove this user's access to a particular notebook. + """ + return \ + "delete from user_notebook where user_id = %s and notebook_id = %s;" % ( quote( self.object_id ), quote( notebook_id ) ) + def sql_has_access( self, notebook_id, read_write = False ): """ Return a SQL string to determine whether this user has access to the given notebook. diff --git a/static/js/Wiki.js b/static/js/Wiki.js index 52c63aa..972935a 100644 --- a/static/js/Wiki.js +++ b/static/js/Wiki.js @@ -185,7 +185,7 @@ Wiki.prototype.populate = function ( startup_notes, current_notes, note_read_wri connect_undelete( notebook.object_id ); appendChildNodes( deleted_notebooks, createDOM( "div", - { "id": "deleted_notebook_" + notebook.object_id }, + { "id": "deleted_notebook_" + notebook.object_id, "class": "deleted_notebook_item" }, createDOM( "span", {}, delete_button ), createDOM( "span", {}, undelete_button ), createDOM( "span", {}, notebook.name ) @@ -304,9 +304,9 @@ Wiki.prototype.populate = function ( startup_notes, current_notes, note_read_wri } ); } - var rename_notebook_link = getElement( "delete_notebook_link" ); - if ( rename_notebook_link ) { - connect( rename_notebook_link, "onclick", function ( event ) { + var delete_notebook_link = getElement( "delete_notebook_link" ); + if ( delete_notebook_link ) { + connect( delete_notebook_link, "onclick", function ( event ) { self.delete_notebook(); event.stop(); } ); @@ -1425,7 +1425,6 @@ Wiki.prototype.create_all_notes_link = function ( note_id, note_title ) { } Wiki.prototype.start_notebook_rename = function () { - this.clear_messages(); this.clear_pulldowns(); // if a renaming is already in progress, end the renaming instead of starting one @@ -1539,7 +1538,11 @@ Wiki.prototype.delete_notebook_forever = function ( event, notebook_id ) { removeElement( deleted_notebook_node ); - this.invoker.invoke( "/notebooks/delete", "POST", { + var items = getElementsByTagAndClassName( "div", "deleted_notebook_item" ); + if ( items.length == 0 ) + removeElement( "deleted_notebooks" ); + + this.invoker.invoke( "/notebooks/delete_forever", "POST", { "notebook_id": notebook_id } );