diff --git a/UPGRADE b/UPGRADE new file mode 100644 index 0000000..97034d6 --- /dev/null +++ b/UPGRADE @@ -0,0 +1,12 @@ +To upgrade the Luminotes database from an earlier version, manually apply each +relevant schema delta file within model/delta/ + +For instance, if you were upgrading from version 1.0.1 to 1.0.4, you would +apply the following deltas in order: + + psql -U luminotes luminotes -f model/delta/1.0.2.sql + psql -U luminotes luminotes -f model/delta/1.0.3.sql + psql -U luminotes luminotes -f model/delta/1.0.4.sql + +Any version which does not introduce a schema change does not have a +corresponding schema delta file. diff --git a/controller/Notebooks.py b/controller/Notebooks.py index 333636b..6a3dc63 100644 --- a/controller/Notebooks.py +++ b/controller/Notebooks.py @@ -54,9 +54,11 @@ class Notebooks( object ): parent_id = Valid_id(), revision = Valid_revision(), rename = Valid_bool(), + deleted_id = Valid_id(), user_id = Valid_id( none_okay = True ), ) - def default( self, notebook_id, note_id = None, parent_id = None, revision = None, rename = False, user_id = None ): + def default( self, notebook_id, note_id = None, parent_id = None, revision = None, rename = False, + deleted_id = None, user_id = None ): """ Provide the information necessary to display the page for a particular notebook. If a particular note id is given without a revision, then the most recent version of that note is @@ -70,6 +72,10 @@ class Notebooks( object ): @param parent_id: id of parent notebook to this notebook (optional) @type revision: unicode or NoneType @param revision: revision timestamp of the provided note (optional) + @type rename: bool or NoneType + @param rename: whether this is a new notebook and should be renamed (optional, defaults to False) + @type deleted_id: unicode or NoneType + @param deleted_id: id of the notebook that was just deleted, if any (optional) @type user_id: unicode or NoneType @param user_id: id of current logged-in user (if any) @rtype: unicode @@ -89,6 +95,7 @@ class Notebooks( object ): else: result[ "conversion" ] = u"signup" result[ "rename" ] = rename + result[ "deleted_id" ] = deleted_id return result @@ -725,24 +732,30 @@ class Notebooks( object ): raise Access_error() user = self.__database.load( User, user_id ) + notebook = self.__create_notebook( u"new notebook", user ) + return dict( + redirect = u"/notebooks/%s?rename=true" % notebook.object_id, + ) + + def __create_notebook( self, name, user, commit = True ): # create the notebook along with a trash trash_id = self.__database.next_id( Notebook, commit = False ) trash = Notebook.create( trash_id, u"trash" ) self.__database.save( trash, commit = False ) notebook_id = self.__database.next_id( Notebook, commit = False ) - notebook = Notebook.create( notebook_id, u"new notebook", trash_id ) + notebook = Notebook.create( notebook_id, name, trash_id ) self.__database.save( notebook, commit = False ) # record the fact that the user has access to their new notebook self.__database.execute( user.sql_save_notebook( notebook_id, read_write = True ), commit = False ) self.__database.execute( user.sql_save_notebook( trash_id, read_write = True ), commit = False ) - self.__database.commit() - return dict( - redirect = u"/notebooks/%s?rename=true" % notebook_id, - ) + if commit: + self.__database.commit() + + return notebook @expose( view = Json ) @grab_user_id @@ -766,6 +779,7 @@ class Notebooks( object ): @raise Access_error: the current user doesn't have access to the given notebook @raise Validation_error: one of the arguments is invalid """ + user = self.__database.load( User, user_id ) if not self.__users.check_access( user_id, notebook_id, read_write = True ): raise Access_error() @@ -792,6 +806,95 @@ class Notebooks( object ): return dict() + @expose( view = Json ) + @grab_user_id + @validate( + notebook_id = Valid_id(), + user_id = Valid_id( none_okay = True ), + ) + def delete( self, notebook_id, user_id ): + """ + Delete the given notebook and redirect to a remaining notebook. If there is none, create one. + + @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 { "redirect": remainingnotebookurl } + @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 ) + + # TODO: maybe if notebook.deleted is already True, then the notebook should be "deleted forever" + if not notebook: + raise Access_error() + + # prevent deletion of a trash notebook directly + if notebook.name == u"trash": + raise Access_error() + + notebook.deleted = True + self.__database.save( notebook, commit = False ) + + # redirect to a remaining undeleted notebook, or if there isn't one, create an empty notebook + remaining_notebook = self.__database.select_one( Notebook, user.sql_load_notebooks( parents_only = True ) ) + if remaining_notebook is None: + remaining_notebook = self.__create_notebook( u"my notebook", user, commit = False ) + + self.__database.commit() + + return dict( + 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 undelete( self, notebook_id, user_id ): + """ + Undelete the given notebook and redirect to it. + + @type notebook_id: unicode + @param notebook_id: id of notebook to undelete + @type user_id: unicode or NoneType + @param user_id: id of current logged-in user (if any) + @rtype dict + @return { "redirect": notebookurl } + @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() + + 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() + + notebook.deleted = False + self.__database.save( notebook, commit = False ) + self.__database.commit() + + return dict( + redirect = u"/notebooks/%s" % notebook.object_id, + ) + def load_recent_notes( self, notebook_id, start = 0, count = 10, user_id = None ): """ Provide the information necessary to display the page for a particular notebook's most recent diff --git a/controller/test/Test_controller.py b/controller/test/Test_controller.py index ed5d57a..de95a87 100644 --- a/controller/test/Test_controller.py +++ b/controller/test/Test_controller.py @@ -26,7 +26,7 @@ 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_load_notebooks( self, parents_only, database ): + def sql_load_notebooks( self, parents_only, deleted, database ): notebooks = [] notebook_tuples = database.user_notebook.get( self.object_id ) @@ -38,12 +38,14 @@ class Test_controller( object ): notebook._Notebook__read_write = read_write if parents_only and notebook.trash_id is None: continue + if deleted != notebook.deleted: + continue notebooks.append( notebook ) return notebooks - User.sql_load_notebooks = lambda self, parents_only = False: \ - lambda database: sql_load_notebooks( self, parents_only, database ) + User.sql_load_notebooks = lambda self, parents_only = False, deleted = False: \ + lambda database: sql_load_notebooks( self, parents_only, deleted, database ) def sql_load_by_username( username, database ): users = [] diff --git a/controller/test/Test_notebooks.py b/controller/test/Test_notebooks.py index d431350..8666794 100644 --- a/controller/test/Test_notebooks.py +++ b/controller/test/Test_notebooks.py @@ -1760,6 +1760,159 @@ class Test_notebooks( Test_controller ): assert result[ u"error" ] + def test_delete( self ): + self.login() + + result = self.http_post( "/notebooks/delete", dict( + notebook_id = self.notebook.object_id, + ), session_id = self.session_id ) + + assert result[ u"redirect" ].startswith( u"/notebooks/" ) + + # assert that we're redirected to a newly created notebook + remaining_notebook_id = result[ u"redirect" ].split( u"/notebooks/" )[ -1 ].split( u"?" )[ 0 ] + notebook = self.database.last_saved_obj + + assert isinstance( notebook, Notebook ) + assert notebook.object_id == remaining_notebook_id + assert notebook.name == u"my notebook" + assert notebook.read_write == True + assert notebook.trash_id + + def test_delete_with_multiple_notebooks( self ): + # create a second notebook, which we should be redirected to after the first notebook is deleted + trash = Notebook.create( self.database.next_id( Notebook ), u"trash" ) + self.database.save( trash, commit = False ) + notebook = Notebook.create( self.database.next_id( Notebook ), u"notebook", trash.object_id ) + self.database.save( notebook, commit = False ) + self.database.execute( self.user.sql_save_notebook( notebook.object_id, read_write = True ) ) + self.database.execute( self.user.sql_save_notebook( notebook.trash_id, read_write = True ) ) + self.database.commit() + + self.login() + + result = self.http_post( "/notebooks/delete", dict( + notebook_id = self.notebook.object_id, + ), session_id = self.session_id ) + + assert result[ u"redirect" ].startswith( u"/notebooks/" ) + + # assert that we're redirected to the second notebook + remaining_notebook_id = result[ u"redirect" ].split( u"/notebooks/" )[ -1 ].split( u"?" )[ 0 ] + assert remaining_notebook_id + assert remaining_notebook_id == notebook.object_id + + def test_contents_after_delete( self ): + self.login() + + result = 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_delete_without_login( self ): + result = self.http_post( "/notebooks/delete", dict( + notebook_id = self.notebook.object_id, + ), session_id = self.session_id ) + + assert result[ u"error" ] + + def test_delete_trash( self ): + self.login() + + result = self.http_post( "/notebooks/delete", dict( + notebook_id = self.notebook.trash_id, + ), session_id = self.session_id ) + + assert u"error" in result + + def test_undelete( self ): + self.login() + + self.http_post( "/notebooks/delete", dict( + notebook_id = self.notebook.object_id, + ), session_id = self.session_id ) + + result = self.http_post( "/notebooks/undelete", dict( + notebook_id = self.notebook.object_id, + ), session_id = self.session_id ) + + assert result[ u"redirect" ].startswith( u"/notebooks/" ) + + # assert that we're redirected to the undeleted notebook + notebook_id = result[ u"redirect" ].split( u"/notebooks/" )[ -1 ] + notebook = self.database.last_saved_obj + + assert isinstance( notebook, Notebook ) + assert notebook.object_id == notebook_id + assert notebook.name == self.notebook.name + assert notebook.read_write == True + assert notebook.trash_id + + def test_contents_after_undelete( self ): + self.login() + + self.http_post( "/notebooks/delete", dict( + notebook_id = self.notebook.object_id, + ), session_id = self.session_id ) + + result = self.http_post( "/notebooks/undelete", 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 == False + + def test_undelete_without_login( self ): + self.http_post( "/notebooks/delete", dict( + notebook_id = self.notebook.object_id, + ), session_id = self.session_id ) + + result = self.http_post( "/notebooks/undelete", dict( + notebook_id = self.notebook.object_id, + ), session_id = self.session_id ) + + assert result[ u"error" ] + + def test_undelete_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/undelete", dict( + notebook_id = self.notebook.object_id, + ), session_id = self.session_id ) + + result = self.http_post( "/notebooks/undelete", dict( + notebook_id = self.notebook.object_id, + ), session_id = self.session_id ) + + assert result[ u"redirect" ].startswith( u"/notebooks/" ) + + # assert that we're redirected to the undeleted notebook + notebook_id = result[ u"redirect" ].split( u"/notebooks/" )[ -1 ] + notebook = self.database.last_saved_obj + + assert isinstance( notebook, Notebook ) + assert notebook.object_id == notebook_id + assert notebook.name == self.notebook.name + assert notebook.read_write == True + assert notebook.trash_id + def test_recent_notes( self ): result = cherrypy.root.notebooks.load_recent_notes( self.notebook.object_id, diff --git a/model/Notebook.py b/model/Notebook.py index 3f3d29d..9a95afe 100644 --- a/model/Notebook.py +++ b/model/Notebook.py @@ -12,7 +12,7 @@ class Notebook( Persistent ): WHITESPACE_PATTERN = re.compile( r"\s+" ) SEARCH_OPERATORS = re.compile( r"[&|!()]" ) - def __init__( self, object_id, revision = None, name = None, trash_id = None, read_write = True ): + def __init__( self, object_id, revision = None, name = None, trash_id = None, deleted = False, read_write = True ): """ Create a new notebook with the given id and name. @@ -24,6 +24,8 @@ class Notebook( Persistent ): @param name: name of this notebook (optional) @type trash_id: Notebook or NoneType @param trash_id: id of the notebook where deleted notes from this notebook go to die (optional) + @type deleted: bool or NoneType + @param deleted: whether this notebook is currently deleted (optional, defaults to False) @type read_write: bool or NoneType @param read_write: whether this view of the notebook is currently read-write (optional, defaults to True) @rtype: Notebook @@ -32,10 +34,11 @@ class Notebook( Persistent ): Persistent.__init__( self, object_id, revision ) self.__name = name self.__trash_id = trash_id + self.__deleted = deleted self.__read_write = read_write @staticmethod - def create( object_id, name = None, trash_id = None, read_write = True ): + def create( object_id, name = None, trash_id = None, deleted = False, read_write = True ): """ Convenience constructor for creating a new notebook. @@ -45,6 +48,8 @@ class Notebook( Persistent ): @param name: name of this notebook (optional) @type trash_id: Notebook or NoneType @param trash_id: id of the notebook where deleted notes from this notebook go to die (optional) + @type deleted: bool or NoneType + @param deleted: whether this notebook is currently deleted (optional, defaults to False) @type read_write: bool or NoneType @param read_write: whether this view of the notebook is currently read-write (optional, defaults to True) @rtype: Notebook @@ -71,10 +76,10 @@ class Notebook( Persistent ): def sql_create( self ): return \ - "insert into notebook ( id, revision, name, trash_id ) " + \ - "values ( %s, %s, %s, %s );" % \ + "insert into notebook ( id, revision, name, trash_id, deleted ) " + \ + "values ( %s, %s, %s, %s, %s );" % \ ( quote( self.object_id ), quote( self.revision ), quote( self.__name ), - quote( self.__trash_id ) ) + quote( self.__trash_id ), quote( self.deleted ) ) def sql_update( self ): return self.sql_create() @@ -187,6 +192,7 @@ class Notebook( Persistent ): name = self.__name, trash_id = self.__trash_id, read_write = self.__read_write, + deleted = self.__deleted, ) ) return d @@ -196,8 +202,15 @@ class Notebook( Persistent ): self.update_revision() def __set_read_write( self, read_write ): + # The read_write member isn't actually saved to the database, so setting it doesn't need to + # call update_revision(). self.__read_write = read_write + def __set_deleted( self, deleted ): + self.__deleted = deleted + self.update_revision() + name = property( lambda self: self.__name, __set_name ) trash_id = property( lambda self: self.__trash_id ) read_write = property( lambda self: self.__read_write, __set_read_write ) + deleted = property( lambda self: self.__deleted, __set_deleted ) diff --git a/model/User.py b/model/User.py index 8e39acb..33289d9 100644 --- a/model/User.py +++ b/model/User.py @@ -129,19 +129,24 @@ class User( Persistent ): def sql_load_by_email_address( email_address ): return "select * from luminotes_user_current where email_address = %s;" % quote( email_address ) - def sql_load_notebooks( self, parents_only = False ): + def sql_load_notebooks( self, parents_only = False, deleted = False ): """ Return a SQL string to load a list of the notebooks to which this user has access. """ if parents_only: - parents_only_clause = " and trash_id is not null"; + parents_only_clause = " and trash_id is not null" else: parents_only_clause = "" + if deleted: + deleted_clause = " and deleted = 't'" + else: + deleted_clause = " and deleted = 'f'" + return \ "select notebook_current.*, user_notebook.read_write from user_notebook, notebook_current " + \ - "where user_id = %s%s and user_notebook.notebook_id = notebook_current.id order by revision;" % \ - ( quote( self.object_id ), parents_only_clause ) + "where user_id = %s%s%s and user_notebook.notebook_id = notebook_current.id order by revision;" % \ + ( quote( self.object_id ), parents_only_clause, deleted_clause ) def sql_save_notebook( self, notebook_id, read_write = True ): """ diff --git a/model/delta/1.0.1.sql b/model/delta/1.0.1.sql new file mode 100644 index 0000000..4bae3d7 --- /dev/null +++ b/model/delta/1.0.1.sql @@ -0,0 +1,3 @@ +alter table notebook add column deleted boolean default 'f'; +drop view notebook_current; +create view notebook_current as select notebook.id, notebook.revision, notebook.name, notebook.trash_id, notebook.deleted from notebook where (notebook.revision in (select max(sub_notebook.revision) as max from notebook sub_notebook where (sub_notebook.id = notebook.id))); diff --git a/model/schema.sql b/model/schema.sql index 60906ba..9f6654c 100644 --- a/model/schema.sql +++ b/model/schema.sql @@ -96,7 +96,8 @@ CREATE TABLE notebook ( id text NOT NULL, revision timestamp with time zone NOT NULL, name text, - trash_id text + trash_id text, + deleted boolean DEFAULT false ); @@ -107,7 +108,7 @@ ALTER TABLE public.notebook OWNER TO luminotes; -- CREATE VIEW notebook_current AS - SELECT notebook.id, notebook.revision, notebook.name, notebook.trash_id FROM notebook WHERE (notebook.revision IN (SELECT max(sub_notebook.revision) AS max FROM notebook sub_notebook WHERE (sub_notebook.id = notebook.id))); + SELECT notebook.id, notebook.revision, notebook.name, notebook.trash_id, notebook.deleted FROM notebook WHERE (notebook.revision IN (SELECT max(sub_notebook.revision) AS max FROM notebook sub_notebook WHERE (sub_notebook.id = notebook.id))); ALTER TABLE public.notebook_current OWNER TO luminotes; diff --git a/model/test/Test_notebook.py b/model/test/Test_notebook.py index 19cc38e..8331c99 100644 --- a/model/test/Test_notebook.py +++ b/model/test/Test_notebook.py @@ -12,8 +12,8 @@ class Test_notebook( object ): self.trash_name = u"trash" self.delta = timedelta( seconds = 1 ) - self.trash = Notebook.create( self.trash_id, self.trash_name, read_write = False ) - self.notebook = Notebook.create( self.object_id, self.name, trash_id = self.trash.object_id ) + self.trash = Notebook.create( self.trash_id, self.trash_name, read_write = False, deleted = False ) + self.notebook = Notebook.create( self.object_id, self.name, trash_id = self.trash.object_id, deleted = False ) self.note = Note.create( "19", u"

title

blah" ) def test_create( self ): @@ -22,12 +22,14 @@ class Test_notebook( object ): assert self.notebook.name == self.name assert self.notebook.read_write == True assert self.notebook.trash_id == self.trash_id + assert self.notebook.deleted == False assert self.trash.object_id == self.trash_id assert datetime.now( tz = utc ) - self.trash.revision < self.delta assert self.trash.name == self.trash_name assert self.trash.read_write == False assert self.trash.trash_id == None + assert self.trash.deleted == False def test_set_name( self ): new_name = u"my new notebook" @@ -44,11 +46,19 @@ class Test_notebook( object ): assert self.notebook.read_write == True assert self.notebook.revision == original_revision + def test_set_deleted( self ): + previous_revision = self.notebook.revision + self.notebook.deleted = True + + assert self.notebook.deleted == True + assert self.notebook.revision > previous_revision + def test_to_dict( self ): d = self.notebook.to_dict() assert d.get( "name" ) == self.name assert d.get( "trash_id" ) == self.trash.object_id assert d.get( "read_write" ) == True + assert d.get( "deleted" ) == self.notebook.deleted assert d.get( "object_id" ) == self.notebook.object_id assert datetime.now( tz = utc ) - d.get( "revision" ) < self.delta diff --git a/static/js/Wiki.js b/static/js/Wiki.js index 75dd9a0..d0e24d6 100644 --- a/static/js/Wiki.js +++ b/static/js/Wiki.js @@ -39,6 +39,23 @@ function Wiki( invoker ) { alert( "Luminotes does not currently support the " + unsupported_agent + " web browser for editing. If possible, please use Firefox or Internet Explorer instead. " + unsupported_agent + " support will be added in a future release. Sorry for the inconvenience." ); } + // if a notebook was just deleted, show a message with an undo button + var deleted_id = getElement( "deleted_id" ).value; + if ( deleted_id && this.notebook.read_write ) { + var undo_button = createDOM( "input", { + "type": "button", + "class": "message_button", + "value": "undo", + "title": "undo deletion" + } ); + var trash_link = createDOM( "a", { + "href": "/notebooks/" + this.notebook.trash_id + "?parent_id=" + this.notebook.object_id + }, "trash" ); + var message_div = this.display_message( "The notebook has been moved to the", [ trash_link, ". ", undo_button ] ); + var self = this; + connect( undo_button, "onclick", function ( event ) { self.undelete_notebook_via_undo( event, deleted_id, message_div ); } ); + } + // populate the wiki with startup notes this.populate( evalJSON( getElement( "startup_notes" ).value || "null" ), @@ -78,7 +95,7 @@ function Wiki( invoker ) { } var rename = evalJSON( getElement( "rename" ).value ); - if ( rename ) + if ( rename && this.notebook.read_write ) this.start_notebook_rename(); } @@ -235,6 +252,14 @@ Wiki.prototype.populate = function ( startup_notes, current_notes, note_read_wri event.stop(); } ); } + + var rename_notebook_link = getElement( "delete_notebook_link" ); + if ( rename_notebook_link ) { + connect( rename_notebook_link, "onclick", function ( event ) { + self.delete_notebook(); + event.stop(); + } ); + } } Wiki.prototype.background_clicked = function ( event ) { @@ -945,6 +970,13 @@ Wiki.prototype.undelete_editor_via_undelete = function( event, note_id, position event.stop(); } +Wiki.prototype.undelete_notebook_via_undo = function( event, notebook_id, position_after ) { + this.invoker.invoke( "/notebooks/undelete", "POST", { + "notebook_id": notebook_id, + } ); + + event.stop(); +} Wiki.prototype.compare_versions = function( event, editor, previous_revision ) { this.clear_pulldowns(); @@ -1436,6 +1468,12 @@ Wiki.prototype.end_notebook_rename = function () { } ); } +Wiki.prototype.delete_notebook = function () { + this.invoker.invoke( "/notebooks/delete", "POST", { + "notebook_id": this.notebook_id, + } ); +} + Wiki.prototype.toggle_editor_changes = function ( event, editor ) { // if the pulldown is already open, then just close it var pulldown_id = "changes_" + editor.id; diff --git a/static/js/test/Test_wiki.html b/static/js/test/Test_wiki.html index 0fe40ae..2f6519c 100644 --- a/static/js/test/Test_wiki.html +++ b/static/js/test/Test_wiki.html @@ -84,6 +84,7 @@ function test_Wiki() { +
diff --git a/view/Link_area.py b/view/Link_area.py index 0c7ffcc..b8a0743 100644 --- a/view/Link_area.py +++ b/view/Link_area.py @@ -55,6 +55,16 @@ class Link_area( Div ): class_ = u"link_area_item", ) or None, + ( notebook.name != u"trash" ) and Div( + A( + u"delete notebook", + href = u"#", + id = u"delete_notebook_link", + title = u"Move this notebook to the trash.", + ), + class_ = u"link_area_item", + ) or None, + notebook.trash_id and Div( A( u"trash", diff --git a/view/Main_page.py b/view/Main_page.py index 24b8440..4b2a513 100644 --- a/view/Main_page.py +++ b/view/Main_page.py @@ -28,6 +28,7 @@ class Main_page( Page ): http_url = None, conversion = None, rename = False, + deleted_id = None, ): startup_note_ids = [ startup_note.object_id for startup_note in startup_notes ] @@ -96,6 +97,7 @@ class Main_page( Page ): Input( type = u"hidden", name = u"current_notes", id = u"current_notes", value = json( note_dicts ) ), Input( type = u"hidden", name = u"note_read_write", id = u"note_read_write", value = json( note_read_write ) ), Input( type = u"hidden", name = u"rename", id = u"rename", value = json( rename ) ), + Input( type = u"hidden", name = u"deleted_id", id = u"deleted_id", value = deleted_id ), Div( id = u"status_area", ),