diff --git a/NEWS b/NEWS index 3694d13..0fe3751 100644 --- a/NEWS +++ b/NEWS @@ -1,3 +1,6 @@ +1.2.14: March ??, 2008 + * Added ability to reorder notebooks on the right side of the page. + 1.2.13: March 11, 2008 * When the "all notes" note is the only note open, it now actually hides when the "hide" button is clicked. diff --git a/controller/Notebooks.py b/controller/Notebooks.py index f6bdcf1..00d6ffb 100644 --- a/controller/Notebooks.py +++ b/controller/Notebooks.py @@ -505,7 +505,7 @@ class Notebooks( object ): note.startup = startup if startup: if note.rank is None: - note.rank = self.__database.select_one( float, notebook.sql_highest_rank() ) + 1 + note.rank = self.__database.select_one( float, notebook.sql_highest_note_rank() ) + 1 else: note.rank = None note.user_id = user.object_id @@ -538,7 +538,7 @@ class Notebooks( object ): # otherwise, create a new note else: if startup: - rank = self.__database.select_one( float, notebook.sql_highest_rank() ) + 1 + rank = self.__database.select_one( float, notebook.sql_highest_note_rank() ) + 1 else: rank = None @@ -875,7 +875,8 @@ class Notebooks( object ): 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, owner = True ), commit = False ) + rank = self.__database.select_one( float, user.sql_highest_notebook_rank() ) + 1 + self.__database.execute( user.sql_save_notebook( notebook_id, read_write = True, owner = True, rank = rank ), commit = False ) self.__database.execute( user.sql_save_notebook( trash_id, read_write = True, owner = True ), commit = False ) if commit: @@ -1071,6 +1072,149 @@ class Notebooks( object ): redirect = u"/notebooks/%s" % notebook.object_id, ) + @expose( view = Json ) + @grab_user_id + @validate( + notebook_id = Valid_id(), + user_id = Valid_id( none_okay = True ), + ) + def move_up( self, notebook_id, user_id ): + """ + Reorder the user's notebooks by moving the given notebook up by one. If the notebook is already + first, then wrap it around to be the last notebook. + + @type notebook_id: unicode + @param notebook_id: id of notebook to move up + @type user_id: unicode or NoneType + @param user_id: id of current logged-in user (if any) + @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 + """ + if not self.__users.check_access( user_id, notebook_id ): + raise Access_error() + + user = self.__database.load( User, user_id ) + if not user: + raise Access_error() + + # load the notebooks to which this user has access + notebooks = self.__database.select_many( + Notebook, + user.sql_load_notebooks( parents_only = True, undeleted_only = True ), + ) + if not notebooks: + raise Access_error() + + # find the given notebook and the one previous to it + previous_notebook = None + current_notebook = None + + for notebook in notebooks: + if notebook.object_id == notebook_id: + current_notebook = notebook + break + previous_notebook = notebook + + if current_notebook is None: + raise Access_error() + + # if there is no previous notebook, then the current notebook is first. so, move it after the + # last notebook + if previous_notebook is None: + last_notebook = notebooks[ -1 ] + self.__database.execute( + user.sql_update_notebook_rank( current_notebook.object_id, last_notebook.rank + 1 ), + commit = False, + ) + # otherwise, save the current and previous notebooks back to the database with swapped ranks + else: + self.__database.execute( + user.sql_update_notebook_rank( current_notebook.object_id, previous_notebook.rank ), + commit = False, + ) + self.__database.execute( + user.sql_update_notebook_rank( previous_notebook.object_id, current_notebook.rank ), + commit = False, + ) + + self.__database.commit() + + return dict() + + @expose( view = Json ) + @grab_user_id + @validate( + notebook_id = Valid_id(), + user_id = Valid_id( none_okay = True ), + ) + def move_down( self, notebook_id, user_id ): + """ + Reorder the user's notebooks by moving the given notebook down by one. If the notebook is + already last, then wrap it around to be the first notebook. + + @type notebook_id: unicode + @param notebook_id: id of notebook to move down + @type user_id: unicode or NoneType + @param user_id: id of current logged-in user (if any) + @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 + """ + if not self.__users.check_access( user_id, notebook_id ): + raise Access_error() + + user = self.__database.load( User, user_id ) + if not user: + raise Access_error() + + # load the notebooks to which this user has access + notebooks = self.__database.select_many( + Notebook, + user.sql_load_notebooks( parents_only = True, undeleted_only = True ), + ) + if not notebooks: + raise Access_error() + + # find the given notebook and the one after it + current_notebook = None + next_notebook = None + + for notebook in notebooks: + if notebook.object_id == notebook_id: + current_notebook = notebook + elif current_notebook: + next_notebook = notebook + break + + if current_notebook is None: + raise Access_error() + + # if there is no next notebook, then the current notebook is last. so, move it before the + # first notebook + if next_notebook is None: + first_notebook = notebooks[ 0 ] + self.__database.execute( + user.sql_update_notebook_rank( current_notebook.object_id, first_notebook.rank - 1 ), + commit = False, + ) + # otherwise, save the current and next notebooks back to the database with swapped ranks + else: + self.__database.execute( + user.sql_update_notebook_rank( current_notebook.object_id, next_notebook.rank ), + commit = False, + ) + self.__database.execute( + user.sql_update_notebook_rank( next_notebook.object_id, current_notebook.rank ), + commit = False, + ) + + self.__database.commit() + + return dict() + 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/Users.py b/controller/Users.py index a360dbc..9444604 100644 --- a/controller/Users.py +++ b/controller/Users.py @@ -262,7 +262,7 @@ class Users( object ): self.__database.save( user, commit = False ) # record the fact that the new user has access to their new notebook - self.__database.execute( user.sql_save_notebook( notebook_id, read_write = True, owner = True ), commit = False ) + self.__database.execute( user.sql_save_notebook( notebook_id, read_write = True, owner = True, rank = 0 ), commit = False ) self.__database.execute( user.sql_save_notebook( trash_id, read_write = True, owner = True ), commit = False ) self.__database.commit() @@ -335,7 +335,7 @@ class Users( object ): self.__database.save( user, commit = False ) # record the fact that the new user has access to their new notebook - self.__database.execute( user.sql_save_notebook( notebook_id, read_write = True, owner = True ), commit = False ) + self.__database.execute( user.sql_save_notebook( notebook_id, read_write = True, owner = True, rank = 0 ), commit = False ) self.__database.execute( user.sql_save_notebook( trash_id, read_write = True, owner = True ), commit = False ) self.__database.commit() @@ -966,7 +966,8 @@ class Users( object ): # if the user doesn't already have access to this notebook, then grant access if not self.__database.select_one( bool, user.sql_has_access( notebook.object_id ) ): - self.__database.execute( user.sql_save_notebook( notebook.object_id, invite.read_write, invite.owner ), commit = False ) + rank = self.__database.select_one( float, user.sql_highest_notebook_rank() ) + 1 + self.__database.execute( user.sql_save_notebook( notebook.object_id, invite.read_write, invite.owner, rank = rank ), commit = False ) # the same goes for the trash notebook if not self.__database.select_one( bool, user.sql_has_access( notebook.trash_id ) ): diff --git a/controller/test/Test_controller.py b/controller/test/Test_controller.py index ec41ed7..c457acc 100644 --- a/controller/test/Test_controller.py +++ b/controller/test/Test_controller.py @@ -41,14 +41,14 @@ class Test_controller( object ): # SQL-returning methods in User, Note, and Notebook to return functions that manipulate data in # Stub_database directly instead. This is all a little fragile, but it's better than relying on # the presence of a real database for unit tests. - def sql_save_notebook( self, notebook_id, read_write, owner, database ): + def sql_save_notebook( self, notebook_id, read_write, owner, rank, database ): if self.object_id in database.user_notebook: - database.user_notebook[ self.object_id ].append( ( notebook_id, read_write, owner ) ) + database.user_notebook[ self.object_id ].append( ( notebook_id, read_write, owner, rank ) ) else: - database.user_notebook[ self.object_id ] = [ ( notebook_id, read_write, owner ) ] + database.user_notebook[ self.object_id ] = [ ( notebook_id, read_write, owner, rank ) ] - User.sql_save_notebook = lambda self, notebook_id, read_write = False, owner = False: \ - lambda database: sql_save_notebook( self, notebook_id, read_write, owner, database ) + User.sql_save_notebook = lambda self, notebook_id, read_write = False, owner = False, rank = None: \ + lambda database: sql_save_notebook( self, notebook_id, read_write, owner, rank, database ) def sql_remove_notebook( self, notebook_id, database ): if self.object_id in database.user_notebook: @@ -66,10 +66,11 @@ class Test_controller( object ): if not notebook_infos: return [] for notebook_info in notebook_infos: - ( notebook_id, notebook_read_write, owner ) = notebook_info + ( notebook_id, notebook_read_write, owner, rank ) = notebook_info notebook = database.objects.get( notebook_id )[ -1 ] notebook.read_write = notebook_read_write notebook.owner = owner + notebook.rank = rank if parents_only and notebook.trash_id is None: continue if undeleted_only and notebook.deleted is True: @@ -118,7 +119,7 @@ class Test_controller( object ): def sql_has_access( self, notebook_id, read_write, owner, database ): for ( user_id, notebook_infos ) in database.user_notebook.items(): for notebook_info in notebook_infos: - ( db_notebook_id, db_read_write, db_owner ) = notebook_info + ( db_notebook_id, db_read_write, db_owner, rank ) = notebook_info if self.object_id == user_id and notebook_id == db_notebook_id: if read_write is True and db_read_write is False: @@ -135,23 +136,53 @@ class Test_controller( object ): def sql_update_access( self, notebook_id, read_write, owner, database ): for ( user_id, notebook_infos ) in database.user_notebook.items(): for notebook_info in notebook_infos: - ( db_notebook_id, db_read_write, db_owner ) = notebook_info + ( db_notebook_id, db_read_write, db_owner, rank ) = notebook_info if self.object_id == user_id and notebook_id == db_notebook_id: notebook_infos_copy = list( notebook_infos ) notebook_infos_copy.remove( notebook_info ) - notebook_infos_copy.append( ( notebook_id, read_write, owner ) ) + notebook_infos_copy.append( ( notebook_id, read_write, owner, rank ) ) database.user_notebook[ user_id ] = notebook_infos_copy User.sql_update_access = lambda self, notebook_id, read_write = False, owner = False: \ lambda database: sql_update_access( self, notebook_id, read_write, owner, database ) + def sql_update_notebook_rank( self, notebook_id, rank, database ): + max_rank = -1 + + for ( user_id, notebook_infos ) in database.user_notebook.items(): + for notebook_info in notebook_infos: + ( db_notebook_id, db_read_write, db_owner, db_rank ) = notebook_info + + if self.object_id == user_id and notebook_id == db_notebook_id: + notebook_infos_copy = list( notebook_infos ) + notebook_infos_copy.remove( notebook_info ) + notebook_infos_copy.append( ( db_notebook_id, db_read_write, db_owner, rank ) ) + database.user_notebook[ user_id ] = notebook_infos_copy + + User.sql_update_notebook_rank = lambda self, notebook_id, rank: \ + lambda database: sql_update_notebook_rank( self, notebook_id, rank, database ) + + def sql_highest_notebook_rank( self, database ): + max_rank = -1 + + for ( user_id, notebook_infos ) in database.user_notebook.items(): + for notebook_info in notebook_infos: + ( db_notebook_id, db_read_write, db_owner, db_rank ) = notebook_info + if self.object_id == user_id and db_rank > max_rank: + max_rank = db_rank + + return max_rank + + User.sql_highest_notebook_rank = lambda self: \ + lambda database: sql_highest_notebook_rank( self, database ) + def sql_revoke_invite_access( notebook_id, trash_id, email_address, database ): invites = [] for ( user_id, notebook_infos ) in database.user_notebook.items(): for notebook_info in list( notebook_infos ): - ( db_notebook_id, read_write, owner ) = notebook_info + ( db_notebook_id, read_write, owner, rank ) = notebook_info if db_notebook_id not in ( notebook_id, trash_id ): continue for ( object_id, obj_list ) in database.objects.items(): obj = obj_list[ -1 ] @@ -255,7 +286,7 @@ class Test_controller( object ): Notebook.sql_search_notes = lambda self, search_text: \ lambda database: sql_search_notes( self, search_text, database ) - def sql_highest_rank( self, database ): + def sql_highest_note_rank( self, database ): max_rank = -1 for ( object_id, obj_list ) in database.objects.items(): @@ -265,8 +296,8 @@ class Test_controller( object ): return max_rank - Notebook.sql_highest_rank = lambda self: \ - lambda database: sql_highest_rank( self, database ) + Notebook.sql_highest_note_rank = lambda self: \ + lambda database: sql_highest_note_rank( self, database ) def sql_count_notes( self, database ): count = 0 diff --git a/controller/test/Test_notebooks.py b/controller/test/Test_notebooks.py index adc2237..485c682 100644 --- a/controller/test/Test_notebooks.py +++ b/controller/test/Test_notebooks.py @@ -54,12 +54,12 @@ class Test_notebooks( Test_controller ): self.anon_notebook = Notebook.create( self.database.next_id( Notebook ), u"anon_notebook", user_id = user_id ) self.database.save( self.anon_notebook, commit = False ) - self.database.execute( self.user.sql_save_notebook( self.notebook.object_id, read_write = True, owner = True ) ) - self.database.execute( self.user.sql_save_notebook( self.notebook.trash_id, read_write = True, owner = True ) ) + self.database.execute( self.user.sql_save_notebook( self.notebook.object_id, read_write = True, owner = True, rank = 0 ) ) + self.database.execute( self.user.sql_save_notebook( self.notebook.trash_id, read_write = True, owner = True, rank = 0 ) ) self.database.execute( self.user.sql_save_notebook( self.anon_notebook.object_id, read_write = False, owner = False ) ) - self.database.execute( self.user2.sql_save_notebook( self.notebook.object_id, read_write = True, owner = False ) ) - self.database.execute( self.user2.sql_save_notebook( self.notebook.trash_id, read_write = True, owner = False ) ) + self.database.execute( self.user2.sql_save_notebook( self.notebook.object_id, read_write = True, owner = False, rank = 0 ) ) + self.database.execute( self.user2.sql_save_notebook( self.notebook.trash_id, read_write = True, owner = False, rank = 0 ) ) def make_users( self ): self.user = User.create( self.database.next_id( User ), self.username, self.password, self.email_address ) @@ -2299,6 +2299,11 @@ class Test_notebooks( Test_controller ): assert notebook.owner == True assert notebook.trash_id + self.user.sql_load_notebooks() + notebooks = self.database.select_many( Notebook, self.user.sql_load_notebooks() ) + new_notebook = [ notebook for notebook in notebooks if notebook.object_id == new_notebook_id ][ 0 ] + assert new_notebook.rank == 1 + def test_contents_after_create( self ): self.login() @@ -2423,8 +2428,8 @@ class Test_notebooks( Test_controller ): 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, owner = True ) ) - self.database.execute( self.user.sql_save_notebook( notebook.trash_id, read_write = True, owner = True ) ) + self.database.execute( self.user.sql_save_notebook( notebook.object_id, read_write = True, owner = True, rank = 1 ) ) + self.database.execute( self.user.sql_save_notebook( notebook.trash_id, read_write = True, owner = True, rank = 1 ) ) self.database.commit() self.login() @@ -2447,8 +2452,8 @@ class Test_notebooks( Test_controller ): 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 = False, owner = False ) ) - self.database.execute( self.user.sql_save_notebook( notebook.trash_id, read_write = False, owner = False ) ) + self.database.execute( self.user.sql_save_notebook( notebook.object_id, read_write = False, owner = False, rank = 1 ) ) + self.database.execute( self.user.sql_save_notebook( notebook.trash_id, read_write = False, owner = False, rank = 1 ) ) self.database.commit() self.login() diff --git a/controller/test/Test_users.py b/controller/test/Test_users.py index 684e80e..044ec17 100644 --- a/controller/test/Test_users.py +++ b/controller/test/Test_users.py @@ -68,9 +68,9 @@ class Test_users( Test_controller ): self.user = User.create( self.database.next_id( User ), self.username, self.password, self.email_address ) self.database.save( self.user, commit = False ) - self.database.execute( self.user.sql_save_notebook( notebook_id1, read_write = True, owner = True ), commit = False ) + self.database.execute( self.user.sql_save_notebook( notebook_id1, read_write = True, owner = True, rank = 0 ), commit = False ) self.database.execute( self.user.sql_save_notebook( trash_id1, read_write = True, owner = True ), commit = False ) - self.database.execute( self.user.sql_save_notebook( notebook_id2, read_write = True, owner = True ), commit = False ) + self.database.execute( self.user.sql_save_notebook( notebook_id2, read_write = True, owner = True, rank = 1 ), commit = False ) self.database.execute( self.user.sql_save_notebook( trash_id2, read_write = True, owner = True ), commit = False ) self.user2 = User.create( self.database.next_id( User ), self.username2, self.password2, self.email_address2 ) @@ -143,6 +143,7 @@ class Test_users( Test_controller ): assert notebook.trash_id assert notebook.read_write == True assert notebook.owner == True + assert notebook.rank == 0 notebook = notebooks[ 1 ] assert notebook.object_id == notebooks[ 0 ].trash_id @@ -151,6 +152,7 @@ class Test_users( Test_controller ): assert notebook.trash_id == None assert notebook.read_write == True assert notebook.owner == True + assert notebook.rank == None notebook = notebooks[ 2 ] assert notebook.object_id == self.anon_notebook.object_id @@ -159,6 +161,7 @@ class Test_users( Test_controller ): assert notebook.trash_id == None assert notebook.read_write == False assert notebook.owner == False + assert notebook.rank == None assert result.get( u"login_url" ) is None assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout" @@ -222,6 +225,7 @@ class Test_users( Test_controller ): assert notebook.trash_id assert notebook.read_write == False assert notebook.owner == False + assert notebook.rank == 1 notebook = notebooks.get( self.notebooks[ 0 ].trash_id ) assert notebook.revision @@ -229,6 +233,7 @@ class Test_users( Test_controller ): assert notebook.trash_id == None assert notebook.read_write == False assert notebook.owner == False + assert notebook.rank == None notebook = notebooks.get( self.anon_notebook.object_id ) assert notebook.revision == self.anon_notebook.revision @@ -236,6 +241,7 @@ class Test_users( Test_controller ): assert notebook.trash_id == None assert notebook.read_write == False assert notebook.owner == False + assert notebook.rank == None assert result.get( u"login_url" ) is None assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout" @@ -283,6 +289,7 @@ class Test_users( Test_controller ): assert notebook.trash_id assert notebook.read_write == True assert notebook.owner == True + assert notebook.rank == 0 notebook = notebooks[ 1 ] assert notebook.object_id == notebooks[ 0 ].trash_id @@ -291,6 +298,7 @@ class Test_users( Test_controller ): assert notebook.trash_id == None assert notebook.read_write == True assert notebook.owner == True + assert notebook.rank == None notebook = notebooks[ 2 ] assert notebook.object_id == self.anon_notebook.object_id @@ -299,6 +307,7 @@ class Test_users( Test_controller ): assert notebook.trash_id == None assert notebook.read_write == False assert notebook.owner == False + assert notebook.rank == None assert result.get( u"login_url" ) is None assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout" @@ -379,22 +388,27 @@ class Test_users( Test_controller ): assert result[ u"notebooks" ][ 0 ].name == self.notebooks[ 0 ].name assert result[ u"notebooks" ][ 0 ].read_write == True assert result[ u"notebooks" ][ 0 ].owner == True + assert result[ u"notebooks" ][ 0 ].rank == 0 assert result[ u"notebooks" ][ 1 ].object_id assert result[ u"notebooks" ][ 1 ].name == u"trash" assert result[ u"notebooks" ][ 1 ].read_write == True assert result[ u"notebooks" ][ 1 ].owner == True + assert result[ u"notebooks" ][ 1 ].rank == None assert result[ u"notebooks" ][ 2 ].object_id == self.notebooks[ 1 ].object_id assert result[ u"notebooks" ][ 2 ].name == self.notebooks[ 1 ].name assert result[ u"notebooks" ][ 2 ].read_write == True assert result[ u"notebooks" ][ 2 ].owner == True + assert result[ u"notebooks" ][ 2 ].rank == 1 assert result[ u"notebooks" ][ 3 ].object_id assert result[ u"notebooks" ][ 3 ].name == u"trash" assert result[ u"notebooks" ][ 3 ].read_write == True assert result[ u"notebooks" ][ 3 ].owner == True + assert result[ u"notebooks" ][ 3 ].rank == None assert result[ u"notebooks" ][ 4 ].object_id == self.anon_notebook.object_id assert result[ u"notebooks" ][ 4 ].name == self.anon_notebook.name assert result[ u"notebooks" ][ 4 ].read_write == False assert result[ u"notebooks" ][ 4 ].owner == False + assert result[ u"notebooks" ][ 4 ].rank == None assert result[ u"login_url" ] is None assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout" @@ -412,6 +426,7 @@ class Test_users( Test_controller ): assert result[ u"notebooks" ][ 0 ].name == self.anon_notebook.name assert result[ u"notebooks" ][ 0 ].read_write == False assert result[ u"notebooks" ][ 0 ].owner == False + assert result[ u"notebooks" ][ 0 ].rank == None login_note = self.database.select_one( Note, self.anon_notebook.sql_load_note_by_title( u"login" ) ) assert result[ u"login_url" ] == u"%s/notebooks/%s?note_id=%s" % ( @@ -426,7 +441,7 @@ class Test_users( Test_controller ): assert rate_plan[ u"name" ] == u"super" assert rate_plan[ u"storage_quota_bytes" ] == 1337 * 10 - def test_current_after_login_with_invite_id( self ): + def test_login_with_invite_id( self ): # trick send_invites() into using a fake SMTP server Stub_smtp.reset() smtplib.SMTP = Stub_smtp @@ -461,7 +476,7 @@ class Test_users( Test_controller ): assert cherrypy.root.users.check_access( self.user2.object_id, self.notebooks[ 0 ].object_id ) assert cherrypy.root.users.check_access( self.user2.object_id, self.notebooks[ 0 ].trash_id ) - def test_current_after_login_with_after_login( self ): + def test_login_with_after_login( self ): after_login = u"/foo/bar" result = self.http_post( "/users/login", dict( @@ -473,7 +488,7 @@ class Test_users( Test_controller ): assert result[ u"redirect" ] == after_login - def test_current_after_login_with_after_login_with_full_url( self ): + def test_login_with_after_login_with_full_url( self ): after_login = u"http://this_url/does/not/start/with/a/slash" result = self.http_post( "/users/login", dict( @@ -601,6 +616,7 @@ class Test_users( Test_controller ): assert result[ u"notebooks" ][ 0 ].name == self.anon_notebook.name assert result[ u"notebooks" ][ 0 ].read_write == False assert result[ u"notebooks" ][ 0 ].owner == False + assert result[ u"notebooks" ][ 0 ].rank == None login_note = self.database.select_one( Note, self.anon_notebook.sql_load_note_by_title( u"login" ) ) assert result[ u"login_url" ] == u"%s/notebooks/%s?note_id=%s" % ( @@ -2104,6 +2120,9 @@ class Test_users( Test_controller ): assert result[ u"error" ] def test_convert_invite_to_access( self ): + # start the invitee out with access to one notebook + self.database.execute( self.user2.sql_save_notebook( self.notebooks[ 1 ].object_id, read_write = True, owner = False, rank = 7 ), commit = False ) + # trick send_invites() into using a fake SMTP server Stub_smtp.reset() smtplib.SMTP = Stub_smtp @@ -2144,6 +2163,12 @@ class Test_users( Test_controller ): ) ) assert access is True + self.user.sql_load_notebooks() + notebooks = self.database.select_many( Notebook, self.user2.sql_load_notebooks() ) + new_notebook = [ notebook for notebook in notebooks if notebook.object_id == invite.notebook_id ][ 0 ] + print new_notebook.rank + assert new_notebook.rank == 8 # one higher than the other notebook this user has access to + assert invite.redeemed_user_id == self.user2.object_id def test_convert_invite_to_access_same_user( self ): @@ -3245,6 +3270,7 @@ class Test_users( Test_controller ): assert result[ u"notebooks" ][ 0 ].name == self.notebooks[ 0 ].name assert result[ u"notebooks" ][ 0 ].read_write == True assert result[ u"notebooks" ][ 0 ].owner == True + assert result[ u"notebooks" ][ 0 ].rank == 0 assert result[ u"login_url" ] == None assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout" @@ -3284,6 +3310,7 @@ class Test_users( Test_controller ): assert result[ u"notebooks" ][ 0 ].name == self.notebooks[ 0 ].name assert result[ u"notebooks" ][ 0 ].read_write == True assert result[ u"notebooks" ][ 0 ].owner == True + assert result[ u"notebooks" ][ 0 ].rank == 0 assert result[ u"login_url" ] == None assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout" @@ -3322,6 +3349,7 @@ class Test_users( Test_controller ): assert result[ u"notebooks" ][ 0 ].name == self.notebooks[ 0 ].name assert result[ u"notebooks" ][ 0 ].read_write == True assert result[ u"notebooks" ][ 0 ].owner == True + assert result[ u"notebooks" ][ 0 ].rank == 0 assert result[ u"login_url" ] == None assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout" @@ -3360,6 +3388,7 @@ class Test_users( Test_controller ): assert result[ u"notebooks" ][ 0 ].name == self.notebooks[ 0 ].name assert result[ u"notebooks" ][ 0 ].read_write == True assert result[ u"notebooks" ][ 0 ].owner == True + assert result[ u"notebooks" ][ 0 ].rank == 0 assert result[ u"login_url" ] == None assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout" @@ -3396,6 +3425,7 @@ class Test_users( Test_controller ): assert result[ u"notebooks" ][ 0 ].name == self.notebooks[ 0 ].name assert result[ u"notebooks" ][ 0 ].read_write == True assert result[ u"notebooks" ][ 0 ].owner == True + assert result[ u"notebooks" ][ 0 ].rank == 0 assert result[ u"login_url" ] == None assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout" diff --git a/model/Notebook.py b/model/Notebook.py index 4f95fe5..cd9c99a 100644 --- a/model/Notebook.py +++ b/model/Notebook.py @@ -12,7 +12,8 @@ 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, deleted = False, user_id = None, read_write = True, owner = True ): + def __init__( self, object_id, revision = None, name = None, trash_id = None, deleted = False, + user_id = None, read_write = True, owner = True, rank = None ): """ Create a new notebook with the given id and name. @@ -32,6 +33,8 @@ class Notebook( Persistent ): @param read_write: whether this view of the notebook is currently read-write (optional, defaults to True) @type owner: bool or NoneType @param owner: whether this view of the notebook currently has owner-level access (optional, defaults to True) + @type rank: float or NoneType + @param rank: indicates numeric ordering of this note in relation to other notebooks @rtype: Notebook @return: newly constructed notebook """ @@ -42,9 +45,10 @@ class Notebook( Persistent ): self.__user_id = user_id self.__read_write = read_write self.__owner = owner + self.__rank = rank @staticmethod - def create( object_id, name = None, trash_id = None, deleted = False, user_id = None, read_write = True, owner = True ): + def create( object_id, name = None, trash_id = None, deleted = False, user_id = None, read_write = True, owner = True, rank = None ): """ Convenience constructor for creating a new notebook. @@ -62,10 +66,12 @@ class Notebook( Persistent ): @param read_write: whether this view of the notebook is currently read-write (optional, defaults to True) @type owner: bool or NoneType @param owner: whether this view of the notebook currently has owner-level access (optional, defaults to True) + @type rank: float or NoneType + @param rank: indicates numeric ordering of this note in relation to other notebooks @rtype: Notebook @return: newly constructed notebook """ - return Notebook( object_id, name = name, trash_id = trash_id, user_id = user_id, read_write = read_write, owner = owner ) + return Notebook( object_id, name = name, trash_id = trash_id, user_id = user_id, read_write = read_write, owner = owner, rank = rank ) @staticmethod def sql_load( object_id, revision = None ): @@ -182,7 +188,7 @@ class Notebook( Persistent ): ) as sub; """ % ( quote( search_text ), quote( self.object_id ) ) - def sql_highest_rank( self ): + def sql_highest_note_rank( self ): """ Return a SQL string to determine the highest numbered rank of all notes in this notebook." """ @@ -232,9 +238,15 @@ class Notebook( Persistent ): self.__user_id = user_id self.update_revision() + def __set_rank( self, rank ): + # The rank member isn't actually saved to the database, so setting it doesn't need to + # call update_revision(). + self.__rank = rank + 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 ) owner = property( lambda self: self.__owner, __set_owner ) deleted = property( lambda self: self.__deleted, __set_deleted ) user_id = property( lambda self: self.__user_id, __set_user_id ) + rank = property( lambda self: self.__rank, __set_rank ) diff --git a/model/User.py b/model/User.py index 4c87ac5..082c621 100644 --- a/model/User.py +++ b/model/User.py @@ -149,18 +149,27 @@ class User( Persistent ): read_write_clause = "" return \ - "select notebook_current.*, user_notebook.read_write, user_notebook.owner from user_notebook, notebook_current " + \ - "where user_notebook.user_id = %s%s%s%s and user_notebook.notebook_id = notebook_current.id order by revision;" % \ - ( quote( self.object_id ), parents_only_clause, undeleted_only_clause, read_write_clause ) + """ + select + notebook_current.*, user_notebook.read_write, user_notebook.owner, user_notebook.rank + from + user_notebook, notebook_current + where + user_notebook.user_id = %s%s%s%s and + user_notebook.notebook_id = notebook_current.id + order by user_notebook.rank; + """ % ( quote( self.object_id ), parents_only_clause, undeleted_only_clause, read_write_clause ) - def sql_save_notebook( self, notebook_id, read_write = True, owner = True ): + def sql_save_notebook( self, notebook_id, read_write = True, owner = True, rank = None ): """ Return a SQL string to save the id of a notebook to which this user has access. """ + if rank is None: rank = quote( None ) + return \ - "insert into user_notebook ( user_id, notebook_id, read_write, owner ) values " + \ + "insert into user_notebook ( user_id, notebook_id, read_write, owner, rank ) values " + \ "( %s, %s, %s, %s );" % ( quote( self.object_id ), quote( notebook_id ), quote( read_write and 't' or 'f' ), - quote( owner and 't' or 'f' ) ) + quote( owner and 't' or 'f' ), rank ) def sql_remove_notebook( self, notebook_id ): """ @@ -199,6 +208,20 @@ class User( Persistent ): ( quote( read_write and 't' or 'f' ), quote( owner and 't' or 'f' ), quote( self.object_id ), quote( notebook_id ) ) + def sql_update_notebook_rank( self, notebook_id, rank ): + """ + Return a SQL string to update the user's rank for the given notebook. + """ + return \ + "update user_notebook set rank = %s where user_id = %s and notebook_id = %s;" % \ + ( quote( rank ), quote( self.object_id ), quote( notebook_id ) ) + + def sql_highest_notebook_rank( self ): + """ + Return a SQL string to determine the highest numbered rank of all notebooks the user has access to." + """ + return "select coalesce( max( rank ), -1 ) from user_notebook where user_id = %s;" % quote( self.object_id ) + @staticmethod def sql_revoke_invite_access( notebook_id, trash_id, email_address ): return \ diff --git a/model/delta/1.2.14.sql b/model/delta/1.2.14.sql new file mode 100644 index 0000000..faf6771 --- /dev/null +++ b/model/delta/1.2.14.sql @@ -0,0 +1 @@ +alter table user_notebook add column rank numeric; diff --git a/model/schema.sql b/model/schema.sql index a6e2dc0..0dbf305 100644 --- a/model/schema.sql +++ b/model/schema.sql @@ -165,7 +165,8 @@ CREATE TABLE user_notebook ( user_id text NOT NULL, notebook_id text NOT NULL, read_write boolean DEFAULT false, - "owner" boolean DEFAULT false + "owner" boolean DEFAULT false, + rank numeric ); diff --git a/model/test/Test_notebook.py b/model/test/Test_notebook.py index e7a74a9..c506605 100644 --- a/model/test/Test_notebook.py +++ b/model/test/Test_notebook.py @@ -12,9 +12,12 @@ class Test_notebook( object ): self.trash_name = u"trash" self.user_id = u"me" self.delta = timedelta( seconds = 1 ) + self.read_write = True + self.owner = False + self.rank = 17.5 self.trash = Notebook.create( self.trash_id, self.trash_name, read_write = False, deleted = False, user_id = self.user_id ) - self.notebook = Notebook.create( self.object_id, self.name, trash_id = self.trash.object_id, deleted = False, user_id = self.user_id ) + self.notebook = Notebook.create( self.object_id, self.name, trash_id = self.trash.object_id, deleted = False, user_id = self.user_id, read_write = self.read_write, owner = self.owner, rank = self.rank ) self.note = Note.create( "19", u"