Began work on notebook deletion and subsequent undo/undeletion.
Changed schema slightly to support this. Added a schema delta file and wrote an UPGRADE doc with info on how to upgrade schemas.
This commit is contained in:
parent
c67aba8fbc
commit
cdd971780e
|
@ -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.
|
|
@ -54,9 +54,11 @@ class Notebooks( object ):
|
||||||
parent_id = Valid_id(),
|
parent_id = Valid_id(),
|
||||||
revision = Valid_revision(),
|
revision = Valid_revision(),
|
||||||
rename = Valid_bool(),
|
rename = Valid_bool(),
|
||||||
|
deleted_id = Valid_id(),
|
||||||
user_id = Valid_id( none_okay = True ),
|
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
|
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
|
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)
|
@param parent_id: id of parent notebook to this notebook (optional)
|
||||||
@type revision: unicode or NoneType
|
@type revision: unicode or NoneType
|
||||||
@param revision: revision timestamp of the provided note (optional)
|
@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
|
@type user_id: unicode or NoneType
|
||||||
@param user_id: id of current logged-in user (if any)
|
@param user_id: id of current logged-in user (if any)
|
||||||
@rtype: unicode
|
@rtype: unicode
|
||||||
|
@ -89,6 +95,7 @@ class Notebooks( object ):
|
||||||
else:
|
else:
|
||||||
result[ "conversion" ] = u"signup"
|
result[ "conversion" ] = u"signup"
|
||||||
result[ "rename" ] = rename
|
result[ "rename" ] = rename
|
||||||
|
result[ "deleted_id" ] = deleted_id
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@ -725,24 +732,30 @@ class Notebooks( object ):
|
||||||
raise Access_error()
|
raise Access_error()
|
||||||
|
|
||||||
user = self.__database.load( User, user_id )
|
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
|
# create the notebook along with a trash
|
||||||
trash_id = self.__database.next_id( Notebook, commit = False )
|
trash_id = self.__database.next_id( Notebook, commit = False )
|
||||||
trash = Notebook.create( trash_id, u"trash" )
|
trash = Notebook.create( trash_id, u"trash" )
|
||||||
self.__database.save( trash, commit = False )
|
self.__database.save( trash, commit = False )
|
||||||
|
|
||||||
notebook_id = self.__database.next_id( Notebook, 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 )
|
self.__database.save( notebook, commit = False )
|
||||||
|
|
||||||
# record the fact that the user has access to their new notebook
|
# 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( notebook_id, read_write = True ), commit = False )
|
||||||
self.__database.execute( user.sql_save_notebook( trash_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(
|
if commit:
|
||||||
redirect = u"/notebooks/%s?rename=true" % notebook_id,
|
self.__database.commit()
|
||||||
)
|
|
||||||
|
return notebook
|
||||||
|
|
||||||
@expose( view = Json )
|
@expose( view = Json )
|
||||||
@grab_user_id
|
@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 Access_error: the current user doesn't have access to the given notebook
|
||||||
@raise Validation_error: one of the arguments is invalid
|
@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 ):
|
if not self.__users.check_access( user_id, notebook_id, read_write = True ):
|
||||||
raise Access_error()
|
raise Access_error()
|
||||||
|
|
||||||
|
@ -792,6 +806,95 @@ class Notebooks( object ):
|
||||||
|
|
||||||
return dict()
|
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 ):
|
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
|
Provide the information necessary to display the page for a particular notebook's most recent
|
||||||
|
|
|
@ -26,7 +26,7 @@ class Test_controller( object ):
|
||||||
User.sql_save_notebook = lambda self, notebook_id, read_write = False: \
|
User.sql_save_notebook = lambda self, notebook_id, read_write = False: \
|
||||||
lambda database: sql_save_notebook( self, notebook_id, read_write, database )
|
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 = []
|
notebooks = []
|
||||||
notebook_tuples = database.user_notebook.get( self.object_id )
|
notebook_tuples = database.user_notebook.get( self.object_id )
|
||||||
|
|
||||||
|
@ -38,12 +38,14 @@ class Test_controller( object ):
|
||||||
notebook._Notebook__read_write = read_write
|
notebook._Notebook__read_write = read_write
|
||||||
if parents_only and notebook.trash_id is None:
|
if parents_only and notebook.trash_id is None:
|
||||||
continue
|
continue
|
||||||
|
if deleted != notebook.deleted:
|
||||||
|
continue
|
||||||
notebooks.append( notebook )
|
notebooks.append( notebook )
|
||||||
|
|
||||||
return notebooks
|
return notebooks
|
||||||
|
|
||||||
User.sql_load_notebooks = lambda self, parents_only = False: \
|
User.sql_load_notebooks = lambda self, parents_only = False, deleted = False: \
|
||||||
lambda database: sql_load_notebooks( self, parents_only, database )
|
lambda database: sql_load_notebooks( self, parents_only, deleted, database )
|
||||||
|
|
||||||
def sql_load_by_username( username, database ):
|
def sql_load_by_username( username, database ):
|
||||||
users = []
|
users = []
|
||||||
|
|
|
@ -1760,6 +1760,159 @@ class Test_notebooks( Test_controller ):
|
||||||
|
|
||||||
assert result[ u"error" ]
|
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 ):
|
def test_recent_notes( self ):
|
||||||
result = cherrypy.root.notebooks.load_recent_notes(
|
result = cherrypy.root.notebooks.load_recent_notes(
|
||||||
self.notebook.object_id,
|
self.notebook.object_id,
|
||||||
|
|
|
@ -12,7 +12,7 @@ class Notebook( Persistent ):
|
||||||
WHITESPACE_PATTERN = re.compile( r"\s+" )
|
WHITESPACE_PATTERN = re.compile( r"\s+" )
|
||||||
SEARCH_OPERATORS = re.compile( r"[&|!()]" )
|
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.
|
Create a new notebook with the given id and name.
|
||||||
|
|
||||||
|
@ -24,6 +24,8 @@ class Notebook( Persistent ):
|
||||||
@param name: name of this notebook (optional)
|
@param name: name of this notebook (optional)
|
||||||
@type trash_id: Notebook or NoneType
|
@type trash_id: Notebook or NoneType
|
||||||
@param trash_id: id of the notebook where deleted notes from this notebook go to die (optional)
|
@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
|
@type read_write: bool or NoneType
|
||||||
@param read_write: whether this view of the notebook is currently read-write (optional, defaults to True)
|
@param read_write: whether this view of the notebook is currently read-write (optional, defaults to True)
|
||||||
@rtype: Notebook
|
@rtype: Notebook
|
||||||
|
@ -32,10 +34,11 @@ class Notebook( Persistent ):
|
||||||
Persistent.__init__( self, object_id, revision )
|
Persistent.__init__( self, object_id, revision )
|
||||||
self.__name = name
|
self.__name = name
|
||||||
self.__trash_id = trash_id
|
self.__trash_id = trash_id
|
||||||
|
self.__deleted = deleted
|
||||||
self.__read_write = read_write
|
self.__read_write = read_write
|
||||||
|
|
||||||
@staticmethod
|
@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.
|
Convenience constructor for creating a new notebook.
|
||||||
|
|
||||||
|
@ -45,6 +48,8 @@ class Notebook( Persistent ):
|
||||||
@param name: name of this notebook (optional)
|
@param name: name of this notebook (optional)
|
||||||
@type trash_id: Notebook or NoneType
|
@type trash_id: Notebook or NoneType
|
||||||
@param trash_id: id of the notebook where deleted notes from this notebook go to die (optional)
|
@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
|
@type read_write: bool or NoneType
|
||||||
@param read_write: whether this view of the notebook is currently read-write (optional, defaults to True)
|
@param read_write: whether this view of the notebook is currently read-write (optional, defaults to True)
|
||||||
@rtype: Notebook
|
@rtype: Notebook
|
||||||
|
@ -71,10 +76,10 @@ class Notebook( Persistent ):
|
||||||
|
|
||||||
def sql_create( self ):
|
def sql_create( self ):
|
||||||
return \
|
return \
|
||||||
"insert into notebook ( id, revision, name, trash_id ) " + \
|
"insert into notebook ( id, revision, name, trash_id, deleted ) " + \
|
||||||
"values ( %s, %s, %s, %s );" % \
|
"values ( %s, %s, %s, %s, %s );" % \
|
||||||
( quote( self.object_id ), quote( self.revision ), quote( self.__name ),
|
( 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 ):
|
def sql_update( self ):
|
||||||
return self.sql_create()
|
return self.sql_create()
|
||||||
|
@ -187,6 +192,7 @@ class Notebook( Persistent ):
|
||||||
name = self.__name,
|
name = self.__name,
|
||||||
trash_id = self.__trash_id,
|
trash_id = self.__trash_id,
|
||||||
read_write = self.__read_write,
|
read_write = self.__read_write,
|
||||||
|
deleted = self.__deleted,
|
||||||
) )
|
) )
|
||||||
|
|
||||||
return d
|
return d
|
||||||
|
@ -196,8 +202,15 @@ class Notebook( Persistent ):
|
||||||
self.update_revision()
|
self.update_revision()
|
||||||
|
|
||||||
def __set_read_write( self, read_write ):
|
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
|
self.__read_write = read_write
|
||||||
|
|
||||||
|
def __set_deleted( self, deleted ):
|
||||||
|
self.__deleted = deleted
|
||||||
|
self.update_revision()
|
||||||
|
|
||||||
name = property( lambda self: self.__name, __set_name )
|
name = property( lambda self: self.__name, __set_name )
|
||||||
trash_id = property( lambda self: self.__trash_id )
|
trash_id = property( lambda self: self.__trash_id )
|
||||||
read_write = property( lambda self: self.__read_write, __set_read_write )
|
read_write = property( lambda self: self.__read_write, __set_read_write )
|
||||||
|
deleted = property( lambda self: self.__deleted, __set_deleted )
|
||||||
|
|
|
@ -129,19 +129,24 @@ class User( Persistent ):
|
||||||
def sql_load_by_email_address( email_address ):
|
def sql_load_by_email_address( email_address ):
|
||||||
return "select * from luminotes_user_current where email_address = %s;" % quote( 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.
|
Return a SQL string to load a list of the notebooks to which this user has access.
|
||||||
"""
|
"""
|
||||||
if parents_only:
|
if parents_only:
|
||||||
parents_only_clause = " and trash_id is not null";
|
parents_only_clause = " and trash_id is not null"
|
||||||
else:
|
else:
|
||||||
parents_only_clause = ""
|
parents_only_clause = ""
|
||||||
|
|
||||||
|
if deleted:
|
||||||
|
deleted_clause = " and deleted = 't'"
|
||||||
|
else:
|
||||||
|
deleted_clause = " and deleted = 'f'"
|
||||||
|
|
||||||
return \
|
return \
|
||||||
"select notebook_current.*, user_notebook.read_write from user_notebook, notebook_current " + \
|
"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;" % \
|
"where user_id = %s%s%s and user_notebook.notebook_id = notebook_current.id order by revision;" % \
|
||||||
( quote( self.object_id ), parents_only_clause )
|
( quote( self.object_id ), parents_only_clause, deleted_clause )
|
||||||
|
|
||||||
def sql_save_notebook( self, notebook_id, read_write = True ):
|
def sql_save_notebook( self, notebook_id, read_write = True ):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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)));
|
|
@ -96,7 +96,8 @@ CREATE TABLE notebook (
|
||||||
id text NOT NULL,
|
id text NOT NULL,
|
||||||
revision timestamp with time zone NOT NULL,
|
revision timestamp with time zone NOT NULL,
|
||||||
name text,
|
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
|
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;
|
ALTER TABLE public.notebook_current OWNER TO luminotes;
|
||||||
|
|
|
@ -12,8 +12,8 @@ class Test_notebook( object ):
|
||||||
self.trash_name = u"trash"
|
self.trash_name = u"trash"
|
||||||
self.delta = timedelta( seconds = 1 )
|
self.delta = timedelta( seconds = 1 )
|
||||||
|
|
||||||
self.trash = Notebook.create( self.trash_id, self.trash_name, read_write = False )
|
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 )
|
self.notebook = Notebook.create( self.object_id, self.name, trash_id = self.trash.object_id, deleted = False )
|
||||||
self.note = Note.create( "19", u"<h3>title</h3>blah" )
|
self.note = Note.create( "19", u"<h3>title</h3>blah" )
|
||||||
|
|
||||||
def test_create( self ):
|
def test_create( self ):
|
||||||
|
@ -22,12 +22,14 @@ class Test_notebook( object ):
|
||||||
assert self.notebook.name == self.name
|
assert self.notebook.name == self.name
|
||||||
assert self.notebook.read_write == True
|
assert self.notebook.read_write == True
|
||||||
assert self.notebook.trash_id == self.trash_id
|
assert self.notebook.trash_id == self.trash_id
|
||||||
|
assert self.notebook.deleted == False
|
||||||
|
|
||||||
assert self.trash.object_id == self.trash_id
|
assert self.trash.object_id == self.trash_id
|
||||||
assert datetime.now( tz = utc ) - self.trash.revision < self.delta
|
assert datetime.now( tz = utc ) - self.trash.revision < self.delta
|
||||||
assert self.trash.name == self.trash_name
|
assert self.trash.name == self.trash_name
|
||||||
assert self.trash.read_write == False
|
assert self.trash.read_write == False
|
||||||
assert self.trash.trash_id == None
|
assert self.trash.trash_id == None
|
||||||
|
assert self.trash.deleted == False
|
||||||
|
|
||||||
def test_set_name( self ):
|
def test_set_name( self ):
|
||||||
new_name = u"my new notebook"
|
new_name = u"my new notebook"
|
||||||
|
@ -44,11 +46,19 @@ class Test_notebook( object ):
|
||||||
assert self.notebook.read_write == True
|
assert self.notebook.read_write == True
|
||||||
assert self.notebook.revision == original_revision
|
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 ):
|
def test_to_dict( self ):
|
||||||
d = self.notebook.to_dict()
|
d = self.notebook.to_dict()
|
||||||
|
|
||||||
assert d.get( "name" ) == self.name
|
assert d.get( "name" ) == self.name
|
||||||
assert d.get( "trash_id" ) == self.trash.object_id
|
assert d.get( "trash_id" ) == self.trash.object_id
|
||||||
assert d.get( "read_write" ) == True
|
assert d.get( "read_write" ) == True
|
||||||
|
assert d.get( "deleted" ) == self.notebook.deleted
|
||||||
assert d.get( "object_id" ) == self.notebook.object_id
|
assert d.get( "object_id" ) == self.notebook.object_id
|
||||||
assert datetime.now( tz = utc ) - d.get( "revision" ) < self.delta
|
assert datetime.now( tz = utc ) - d.get( "revision" ) < self.delta
|
||||||
|
|
|
@ -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." );
|
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
|
// populate the wiki with startup notes
|
||||||
this.populate(
|
this.populate(
|
||||||
evalJSON( getElement( "startup_notes" ).value || "null" ),
|
evalJSON( getElement( "startup_notes" ).value || "null" ),
|
||||||
|
@ -78,7 +95,7 @@ function Wiki( invoker ) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var rename = evalJSON( getElement( "rename" ).value );
|
var rename = evalJSON( getElement( "rename" ).value );
|
||||||
if ( rename )
|
if ( rename && this.notebook.read_write )
|
||||||
this.start_notebook_rename();
|
this.start_notebook_rename();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -235,6 +252,14 @@ Wiki.prototype.populate = function ( startup_notes, current_notes, note_read_wri
|
||||||
event.stop();
|
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 ) {
|
Wiki.prototype.background_clicked = function ( event ) {
|
||||||
|
@ -945,6 +970,13 @@ Wiki.prototype.undelete_editor_via_undelete = function( event, note_id, position
|
||||||
event.stop();
|
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 ) {
|
Wiki.prototype.compare_versions = function( event, editor, previous_revision ) {
|
||||||
this.clear_pulldowns();
|
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 ) {
|
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;
|
||||||
|
|
|
@ -84,6 +84,7 @@ function test_Wiki() {
|
||||||
<input type="hidden" name="note" id="note" value="" />
|
<input type="hidden" name="note" id="note" value="" />
|
||||||
<input type="hidden" name="note_read_write" id="note_read_write" value="" />
|
<input type="hidden" name="note_read_write" id="note_read_write" value="" />
|
||||||
<input type="hidden" name="rename" id="rename" value="false" />
|
<input type="hidden" name="rename" id="rename" value="false" />
|
||||||
|
<input type="hidden" name="deleted_id" id="deleted_id" value="" />
|
||||||
|
|
||||||
<div id="static_notes">
|
<div id="static_notes">
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -55,6 +55,16 @@ class Link_area( Div ):
|
||||||
class_ = u"link_area_item",
|
class_ = u"link_area_item",
|
||||||
) or None,
|
) 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(
|
notebook.trash_id and Div(
|
||||||
A(
|
A(
|
||||||
u"trash",
|
u"trash",
|
||||||
|
|
|
@ -28,6 +28,7 @@ class Main_page( Page ):
|
||||||
http_url = None,
|
http_url = None,
|
||||||
conversion = None,
|
conversion = None,
|
||||||
rename = False,
|
rename = False,
|
||||||
|
deleted_id = None,
|
||||||
):
|
):
|
||||||
startup_note_ids = [ startup_note.object_id for startup_note in startup_notes ]
|
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"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"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"rename", id = u"rename", value = json( rename ) ),
|
||||||
|
Input( type = u"hidden", name = u"deleted_id", id = u"deleted_id", value = deleted_id ),
|
||||||
Div(
|
Div(
|
||||||
id = u"status_area",
|
id = u"status_area",
|
||||||
),
|
),
|
||||||
|
|
Reference in New Issue