From 36af0bbfedf8b1677e94d81cf95a12070022cdf2 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Fri, 24 Oct 2008 15:07:36 -0700 Subject: [PATCH] Propagating notebook tags to the UI code. --- controller/Notebooks.py | 12 +++- controller/Users.py | 10 ++- controller/test/Test_notebooks.py | 94 ++++++++++++++++++++++++++++ controller/test/Test_users.py | 100 ++++++++++++++++++++++++++++++ model/Notebook.py | 8 +++ model/test/Test_notebook.py | 17 +++++ view/Link_area.py | 35 +++++++---- view/Main_page.py | 1 + 8 files changed, 260 insertions(+), 17 deletions(-) diff --git a/controller/Notebooks.py b/controller/Notebooks.py index 6c421d2..1b932bb 100644 --- a/controller/Notebooks.py +++ b/controller/Notebooks.py @@ -18,6 +18,7 @@ from model.Invite import Invite from model.User import User from model.User_revision import User_revision from model.File import File +from model.Tag import Tag from view.Main_page import Main_page from view.Json import Json from view.Html_file import Html_file @@ -168,8 +169,8 @@ class Notebooks( object ): def contents( self, notebook_id, note_id = None, revision = None, previous_revision = None, read_write = True, owner = True, user_id = None ): """ - Return the startup notes for the given notebook. Optionally include a single requested note as - well. + Return information about the requested notebook, including its startup notes. Optionally include + a single requested note as well. @type notebook_id: unicode @param notebook_id: id of notebook to return @@ -199,8 +200,9 @@ class Notebooks( object ): @raise Validation_error: one of the arguments is invalid """ notebook = self.__users.load_notebook( user_id, notebook_id ) + anonymous = self.__database.select_one( User, User.sql_load_by_username( u"anonymous" ), use_cache = True ) - if notebook is None: + if notebook is None or anonymous is None: raise Access_error() if read_write is False: @@ -225,6 +227,10 @@ class Notebooks( object ): else: note = None + notebook.tags = \ + self.__database.select_many( Tag, notebook.sql_load_tags( user_id ) ) + \ + self.__database.select_many( Tag, notebook.sql_load_tags( anonymous.object_id ) ) + startup_notes = self.__database.select_many( Note, notebook.sql_load_startup_notes() ) total_notes_count = self.__database.select_one( int, notebook.sql_count_notes(), use_cache = True ) diff --git a/controller/Users.py b/controller/Users.py index fa53077..1e8ebb7 100644 --- a/controller/Users.py +++ b/controller/Users.py @@ -13,6 +13,7 @@ from model.Note import Note from model.Password_reset import Password_reset from model.Download_access import Download_access from model.Invite import Invite +from model.Tag import Tag from Expose import expose from Validate import validate, Valid_string, Valid_bool, Valid_int, Validation_error from Database import Valid_id, end_transaction @@ -649,9 +650,16 @@ class Users( object ): if login_note: login_url = "%s/notebooks/%s?note_id=%s" % ( self.__https_url, main_notebook.object_id, login_note.object_id ) + notebooks += anon_notebooks + + for notebook in notebooks: + notebook.tags = \ + self.__database.select_many( Tag, notebook.sql_load_tags( user_id ) ) + \ + self.__database.select_many( Tag, notebook.sql_load_tags( anonymous.object_id ) ) + return dict( user = user, - notebooks = notebooks + anon_notebooks, + notebooks = notebooks, login_url = login_url, logout_url = self.__https_url + u"/users/logout", rate_plan = ( user.rate_plan < len( self.__rate_plans ) ) and self.__rate_plans[ user.rate_plan ] or {}, diff --git a/controller/test/Test_notebooks.py b/controller/test/Test_notebooks.py index c9b5388..2f51e79 100644 --- a/controller/test/Test_notebooks.py +++ b/controller/test/Test_notebooks.py @@ -14,6 +14,7 @@ from model.Note import Note from model.User import User from model.Invite import Invite from model.File import File +from model.Tag import Tag from controller.Notebooks import Access_error from controller.Files import Upload_file @@ -3850,6 +3851,99 @@ class Test_notebooks( Test_controller ): assert notebook.object_id == new_notebook_id assert notebook.read_write == Notebook.READ_WRITE assert notebook.owner == True + assert notebook.tags == [] + + def test_contents_after_create_with_tag( self ): + self.login() + + result = self.http_post( "/notebooks/create", dict(), session_id = self.session_id ) + new_notebook_id = result[ u"redirect" ].split( u"/notebooks/" )[ -1 ].split( u"?" )[ 0 ] + + tag_id = self.database.next_id( Tag, commit = False ) + new_tag = Tag.create( + tag_id, + notebook_id = None, # this tag is not in the namespace of a single notebook + user_id = self.user.object_id, + name = u"mytag", + description = u"some tag" + ) + self.database.save( new_tag, commit = False ) + + self.database.execute( + self.user.sql_save_notebook_tag( new_notebook_id, new_tag.object_id, value = u"myvalue" ), + commit = False, + ) + self.database.commit() + + result = cherrypy.root.notebooks.contents( + notebook_id = new_notebook_id, + user_id = self.user.object_id, + ) + + notebook = result[ "notebook" ] + assert result[ "total_notes_count" ] == 0 + assert result[ "startup_notes" ] == [] + assert result[ "notes" ] == [] + assert result[ "invites" ] == [] + + assert notebook.object_id == new_notebook_id + assert notebook.read_write == Notebook.READ_WRITE + assert notebook.owner == True + assert notebook.tags + assert len( notebook.tags ) == 1 + + tag = notebook.tags[ 0 ] + assert tag.object_id == new_tag.object_id + assert tag.notebook_id == new_tag.notebook_id + assert tag.user_id == new_tag.user_id + assert tag.name == new_tag.name + assert tag.description == new_tag.description + + def test_contents_after_create_with_anonymous_tag( self ): + self.login() + + result = self.http_post( "/notebooks/create", dict(), session_id = self.session_id ) + new_notebook_id = result[ u"redirect" ].split( u"/notebooks/" )[ -1 ].split( u"?" )[ 0 ] + + tag_id = self.database.next_id( Tag, commit = False ) + new_tag = Tag.create( + tag_id, + notebook_id = None, # this tag is not in the namespace of a single notebook + user_id = self.anonymous.object_id, + name = u"mytag", + description = u"some tag" + ) + self.database.save( new_tag, commit = False ) + + self.database.execute( + self.anonymous.sql_save_notebook_tag( new_notebook_id, new_tag.object_id, value = u"myvalue" ), + commit = False, + ) + self.database.commit() + + result = cherrypy.root.notebooks.contents( + notebook_id = new_notebook_id, + user_id = self.user.object_id, + ) + + notebook = result[ "notebook" ] + assert result[ "total_notes_count" ] == 0 + assert result[ "startup_notes" ] == [] + assert result[ "notes" ] == [] + assert result[ "invites" ] == [] + + assert notebook.object_id == new_notebook_id + assert notebook.read_write == Notebook.READ_WRITE + assert notebook.owner == True + assert notebook.tags + assert len( notebook.tags ) == 1 + + tag = notebook.tags[ 0 ] + assert tag.object_id == new_tag.object_id + assert tag.notebook_id == new_tag.notebook_id + assert tag.user_id == new_tag.user_id + assert tag.name == new_tag.name + assert tag.description == new_tag.description def test_create_without_login( self ): result = self.http_post( "/notebooks/create", dict() ) diff --git a/controller/test/Test_users.py b/controller/test/Test_users.py index 1aaf6fa..3da6928 100644 --- a/controller/test/Test_users.py +++ b/controller/test/Test_users.py @@ -15,6 +15,7 @@ from model.Note import Note from model.Password_reset import Password_reset from model.Download_access import Download_access from model.Invite import Invite +from model.Tag import Tag from controller.Users import Invite_error, Payment_error import controller.Users as Users @@ -745,26 +746,125 @@ class Test_users( Test_controller ): assert result[ u"notebooks" ][ 0 ].read_write == Notebook.READ_WRITE assert result[ u"notebooks" ][ 0 ].owner == True assert result[ u"notebooks" ][ 0 ].rank == None + assert result[ u"notebooks" ][ 0 ].tags == [] assert result[ u"notebooks" ][ 1 ].object_id assert result[ u"notebooks" ][ 1 ].name == u"trash" assert result[ u"notebooks" ][ 1 ].read_write == Notebook.READ_WRITE assert result[ u"notebooks" ][ 1 ].owner == True assert result[ u"notebooks" ][ 1 ].rank == None + assert result[ u"notebooks" ][ 1 ].tags == [] assert result[ u"notebooks" ][ 2 ].object_id == self.notebooks[ 0 ].object_id assert result[ u"notebooks" ][ 2 ].name == self.notebooks[ 0 ].name assert result[ u"notebooks" ][ 2 ].read_write == Notebook.READ_WRITE assert result[ u"notebooks" ][ 2 ].owner == True assert result[ u"notebooks" ][ 2 ].rank == 0 + assert result[ u"notebooks" ][ 2 ].tags == [] assert result[ u"notebooks" ][ 3 ].object_id == self.notebooks[ 1 ].object_id assert result[ u"notebooks" ][ 3 ].name == self.notebooks[ 1 ].name assert result[ u"notebooks" ][ 3 ].read_write == Notebook.READ_WRITE assert result[ u"notebooks" ][ 3 ].owner == True assert result[ u"notebooks" ][ 3 ].rank == 1 + assert result[ u"notebooks" ][ 3 ].tags == [] 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 == Notebook.READ_ONLY assert result[ u"notebooks" ][ 4 ].owner == False assert result[ u"notebooks" ][ 4 ].rank == None + assert result[ u"notebooks" ][ 4 ].tags == [] + assert result[ u"login_url" ] is None + assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout" + + rate_plan = result[ u"rate_plan" ] + assert rate_plan + assert rate_plan[ u"name" ] == u"super" + assert rate_plan[ u"storage_quota_bytes" ] == 1337 * 10 + + assert result[ u"groups" ] + assert result[ u"groups" ][ 0 ].object_id == self.group.object_id + assert result[ u"groups" ][ 0 ].name == self.group.name + assert result[ u"groups" ][ 0 ].admin == False + + def test_current_with_tags( self ): + tag_id = self.database.next_id( Tag, commit = False ) + new_tag = Tag.create( + tag_id, + notebook_id = None, # this tag is not in the namespace of a single notebook + user_id = self.anonymous.object_id, + name = u"mytag", + description = u"some tag" + ) + self.database.save( new_tag, commit = False ) + self.database.execute( + self.user.sql_save_notebook_tag( self.notebooks[ 0 ].object_id, new_tag.object_id, value = u"myvalue" ), + commit = False, + ) + + tag_id2 = self.database.next_id( Tag, commit = False ) + new_tag2 = Tag.create( + tag_id2, + notebook_id = None, # this tag is not in the namespace of a single notebook + user_id = self.user.object_id, + name = u"mytag2", + description = u"some tag 2" + ) + self.database.save( new_tag2, commit = False ) + self.database.execute( + self.user.sql_save_notebook_tag( self.notebooks[ 0 ].object_id, new_tag2.object_id, value = u"myvalue2" ), + commit = False, + ) + + self.database.commit() + + result = cherrypy.root.users.current( self.user.object_id ) + + assert result[ u"user" ] + assert result[ u"user" ].object_id == self.user.object_id + assert result[ u"user" ].username == self.user.username + assert len( result[ u"notebooks" ] ) == 5 + assert result[ u"notebooks" ][ 0 ].object_id + assert result[ u"notebooks" ][ 0 ].name == u"trash" + assert result[ u"notebooks" ][ 0 ].read_write == Notebook.READ_WRITE + assert result[ u"notebooks" ][ 0 ].owner == True + assert result[ u"notebooks" ][ 0 ].rank == None + assert result[ u"notebooks" ][ 0 ].tags == [] + assert result[ u"notebooks" ][ 1 ].object_id + assert result[ u"notebooks" ][ 1 ].name == u"trash" + assert result[ u"notebooks" ][ 1 ].read_write == Notebook.READ_WRITE + assert result[ u"notebooks" ][ 1 ].owner == True + assert result[ u"notebooks" ][ 1 ].rank == None + assert result[ u"notebooks" ][ 1 ].tags == [] + assert result[ u"notebooks" ][ 2 ].object_id == self.notebooks[ 0 ].object_id + assert result[ u"notebooks" ][ 2 ].name == self.notebooks[ 0 ].name + assert result[ u"notebooks" ][ 2 ].read_write == Notebook.READ_WRITE + assert result[ u"notebooks" ][ 2 ].owner == True + assert result[ u"notebooks" ][ 2 ].rank == 0 + assert result[ u"notebooks" ][ 2 ].tags + assert len( result[ u"notebooks" ][ 2 ].tags ) == 2 + + tags = result[ u"notebooks" ][ 2 ].tags + assert tags[ 0 ].object_id == new_tag.object_id + assert tags[ 0 ].notebook_id == new_tag.notebook_id + assert tags[ 0 ].user_id == new_tag.user_id + assert tags[ 0 ].name == new_tag.name + assert tags[ 0 ].description == new_tag.description + assert tags[ 1 ].object_id == new_tag2.object_id + assert tags[ 1 ].notebook_id == new_tag2.notebook_id + assert tags[ 1 ].user_id == new_tag2.user_id + assert tags[ 1 ].name == new_tag2.name + assert tags[ 1 ].description == new_tag2.description + + assert result[ u"notebooks" ][ 3 ].object_id == self.notebooks[ 1 ].object_id + assert result[ u"notebooks" ][ 3 ].name == self.notebooks[ 1 ].name + assert result[ u"notebooks" ][ 3 ].read_write == Notebook.READ_WRITE + assert result[ u"notebooks" ][ 3 ].owner == True + assert result[ u"notebooks" ][ 3 ].rank == 1 + assert result[ u"notebooks" ][ 3 ].tags == [] + 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 == Notebook.READ_ONLY + assert result[ u"notebooks" ][ 4 ].owner == False + assert result[ u"notebooks" ][ 4 ].rank == None + assert result[ u"notebooks" ][ 4 ].tags == [] assert result[ u"login_url" ] is 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 da290fe..d4a24cc 100644 --- a/model/Notebook.py +++ b/model/Notebook.py @@ -63,6 +63,7 @@ class Notebook( Persistent ): self.__read_write = read_write self.__owner = owner self.__rank = rank + self.__tags = [] @staticmethod def create( object_id, name = None, trash_id = None, deleted = False, user_id = None, read_write = None, owner = True, rank = None, own_notes_only = False ): @@ -330,6 +331,7 @@ class Notebook( Persistent ): owner = self.__owner, deleted = self.__deleted, user_id = self.__user_id, + tags = self.__tags, ) ) return d @@ -367,6 +369,11 @@ class Notebook( Persistent ): # call update_revision(). self.__rank = rank + def __set_tags( self, tags ): + # The tags member isn't actually saved to the database, so setting it doesn't need to + # call update_revision(). + self.__tags = tags + 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 ) @@ -374,3 +381,4 @@ class Notebook( Persistent ): 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 ) + tags = property( lambda self: self.__tags, __set_tags ) diff --git a/model/test/Test_notebook.py b/model/test/Test_notebook.py index 3d7ee0b..c1a5ebb 100644 --- a/model/test/Test_notebook.py +++ b/model/test/Test_notebook.py @@ -30,6 +30,7 @@ class Test_notebook( object ): assert self.notebook.read_write == self.read_write assert self.notebook.owner == self.owner assert self.notebook.rank == self.rank + assert self.notebook.tags == [] assert self.trash.object_id == self.trash_id assert datetime.now( tz = utc ) - self.trash.revision < self.delta @@ -40,6 +41,7 @@ class Test_notebook( object ): assert self.trash.read_write == Notebook.READ_ONLY assert self.trash.owner == True assert self.trash.rank == None + assert self.trash.tags == [] def test_create_read_write_true( self ): notebook = Notebook.create( self.object_id, self.name, trash_id = None, deleted = False, user_id = self.user_id, read_write = True, owner = self.owner, rank = self.rank ) @@ -53,6 +55,7 @@ class Test_notebook( object ): assert notebook.read_write == Notebook.READ_WRITE assert notebook.owner == self.owner assert notebook.rank == self.rank + assert notebook.tags == [] def test_create_read_write_false( self ): notebook = Notebook.create( self.object_id, self.name, trash_id = None, deleted = False, user_id = self.user_id, read_write = False, owner = self.owner, rank = self.rank ) @@ -66,6 +69,7 @@ class Test_notebook( object ): assert notebook.read_write == Notebook.READ_ONLY assert notebook.owner == self.owner assert notebook.rank == self.rank + assert notebook.tags == [] def test_create_read_write_none( self ): notebook = Notebook.create( self.object_id, self.name, trash_id = None, deleted = False, user_id = self.user_id, read_write = None, owner = self.owner, rank = self.rank ) @@ -79,6 +83,7 @@ class Test_notebook( object ): assert notebook.read_write == Notebook.READ_WRITE assert notebook.owner == self.owner assert notebook.rank == self.rank + assert notebook.tags == [] def test_create_read_write_true_and_own_notes_only_true( self ): notebook = Notebook.create( self.object_id, self.name, trash_id = None, deleted = False, user_id = self.user_id, read_write = True, owner = self.owner, rank = self.rank, own_notes_only = True ) @@ -92,6 +97,7 @@ class Test_notebook( object ): assert notebook.read_write == Notebook.READ_WRITE_FOR_OWN_NOTES assert notebook.owner == self.owner assert notebook.rank == self.rank + assert notebook.tags == [] def test_create_read_write_false_and_own_notes_only_true( self ): notebook = Notebook.create( self.object_id, self.name, trash_id = None, deleted = False, user_id = self.user_id, read_write = False, owner = self.owner, rank = self.rank, own_notes_only = True ) @@ -105,6 +111,7 @@ class Test_notebook( object ): assert notebook.read_write == Notebook.READ_ONLY assert notebook.owner == self.owner assert notebook.rank == self.rank + assert notebook.tags == [] def test_create_read_write_false_and_own_notes_only_false( self ): notebook = Notebook.create( self.object_id, self.name, trash_id = None, deleted = False, user_id = self.user_id, read_write = False, owner = self.owner, rank = self.rank, own_notes_only = False ) @@ -118,6 +125,7 @@ class Test_notebook( object ): assert notebook.read_write == Notebook.READ_ONLY assert notebook.owner == self.owner assert notebook.rank == self.rank + assert notebook.tags == [] def test_create_read_write_true_and_own_notes_only_false( self ): notebook = Notebook.create( self.object_id, self.name, trash_id = None, deleted = False, user_id = self.user_id, read_write = True, owner = self.owner, rank = self.rank, own_notes_only = False ) @@ -131,6 +139,7 @@ class Test_notebook( object ): assert notebook.read_write == Notebook.READ_WRITE assert notebook.owner == self.owner assert notebook.rank == self.rank + assert notebook.tags == [] def test_set_name( self ): new_name = u"my new notebook" @@ -189,6 +198,13 @@ class Test_notebook( object ): assert self.notebook.rank == 17.7 assert self.notebook.revision == original_revision + def test_set_tags( self ): + original_revision = self.notebook.revision + self.notebook.tags = [ u"whee", u"blah", u"hm" ] # normally these would be Tag objects + + assert self.notebook.tags == [ u"whee", u"blah", u"hm" ] + assert self.notebook.revision == original_revision + def test_to_dict( self ): d = self.notebook.to_dict() @@ -199,3 +215,4 @@ class Test_notebook( object ): assert d.get( "user_id" ) == self.notebook.user_id assert d.get( "object_id" ) == self.notebook.object_id assert datetime.now( tz = utc ) - d.get( "revision" ) < self.delta + assert d.get( "tags" ) == [] diff --git a/view/Link_area.py b/view/Link_area.py index 09445e1..042312e 100644 --- a/view/Link_area.py +++ b/view/Link_area.py @@ -15,12 +15,21 @@ class Link_area( Div ): nb.deleted is False ] + if [ tag for tag in notebook.tags if tag.name == u"forum" ]: + forum_tag = True + notebook_word = u"discussion" + note_word = u"post" + else: + forum_tag = False + notebook_word = u"notebook" + note_word = u"note" + Div.__init__( self, Div( Div( H4( - u"this notebook", + u"this %s" % notebook_word, notebook.read_write != Notebook.READ_ONLY and Input( type = u"button", class_ = u"note_button small_text", @@ -54,22 +63,22 @@ class Link_area( Div ): u"subscribe to rss", href = updates_path, id = u"notebook_rss_link", - title = u"Subscribe to the RSS feed for this notebook.", + title = u"Subscribe to the RSS feed for this %s." % notebook_word, ), A( Img( src = u"/static/images/rss.png", width = u"14", height = u"14", class_ = u"middle_image" ), href = updates_path, - title = u"Subscribe to the RSS feed for this notebook.", + title = u"Subscribe to the RSS feed for this %s." % notebook_word, ), class_ = u"link_area_item", ) or None ), ( notebook.read_write != Notebook.READ_ONLY ) and Div( A( - u"nothing but notes", + u"nothing but %ss" % note_word, href = u"#", id = u"declutter_link", - title = u"Focus on just your notes without any distractions.", + title = u"Focus on just your %ss without any distractions." % note_word, ), class_ = u"link_area_item", ) or None, @@ -79,7 +88,7 @@ class Link_area( Div ): u"export", href = u"#", id = u"export_link", - title = u"Download a stand-alone copy of the entire wiki notebook.", + title = u"Download a stand-alone copy of the entire %s." % notebook_word, ), class_ = u"link_area_item", ) or None, @@ -90,14 +99,14 @@ class Link_area( Div ): u"import", href = u"#", id = u"import_link", - title = u"Import notes from other software into Luminotes.", + title = u"Import %ss from other software into Luminotes." % note_word, ), u"|", A( u"export", href = u"#", id = u"export_link", - title = u"Download a stand-alone copy of the entire wiki notebook.", + title = u"Download a stand-alone copy of the entire %s." % notebook_word, ), class_ = u"link_area_item", ) or None, @@ -107,7 +116,7 @@ class Link_area( Div ): u"rename", href = u"#", id = u"rename_notebook_link", - title = u"Change the name of this notebook.", + title = u"Change the name of this %s." % notebook_word, ), class_ = u"link_area_item", ) or None, @@ -117,7 +126,7 @@ class Link_area( Div ): u"delete", href = u"#", id = u"delete_notebook_link", - title = u"Move this notebook to the trash.", + title = u"Move this %s to the trash." % notebook_word, ), class_ = u"link_area_item", ) or None, @@ -127,7 +136,7 @@ class Link_area( Div ): u"share", href = u"#", id = u"share_notebook_link", - title = u"Share this notebook with others.", + title = u"Share this %s with others." % notebook_word, ), class_ = u"link_area_item", ) or None, @@ -137,7 +146,7 @@ class Link_area( Div ): u"trash", href = u"/notebooks/%s?parent_id=%s" % ( notebook.trash_id, notebook.object_id ), id = u"trash_link", - title = u"Look here for notes you've deleted.", + title = u"Look here for %ss you've deleted." % note_word, ), class_ = u"link_area_item", ) or None, @@ -148,7 +157,7 @@ class Link_area( Div ): u"trash", href = u"#", id = u"trash_link", - title = u"Look here for notes you've deleted.", + title = u"Look here for %ss you've deleted." % note_word, ), class_ = u"link_area_item", ) or None, diff --git a/view/Main_page.py b/view/Main_page.py index 80d268c..902b66e 100644 --- a/view/Main_page.py +++ b/view/Main_page.py @@ -61,6 +61,7 @@ class Main_page( Page ): u"object_id" : startup_note.object_id, u"revision" : startup_note.revision, u"deleted_from_id" : startup_note.deleted_from_id, + u"user_id": startup_note.user_id, } for startup_note in startup_notes ] note_dicts = [ {