From 5554b1df179ccdbced8dda4bfb06d685c9f5beb6 Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Tue, 11 Dec 2007 01:15:03 +0000 Subject: [PATCH] * Started a static upgrade HTML file with rate plans. * Fixed Invite.sql_update() to have SQL params in proper order. * Fixed bug where email addresses containing "-" were considered invalid. * Made UI for inviting other people to your notebook. * Tweaked the rate plans and added a new one. --- config/Common.py | 16 +++- controller/Users.py | 49 +++++++----- controller/test/Test_users.py | 144 ++++++++++++++++++++++++++-------- model/Invite.py | 4 +- static/css/note.css | 53 +++++++++++++ static/html/upgrade.html | 45 +++++++++++ static/images/check.png | Bin 0 -> 605 bytes static/images/check.xcf | Bin 0 -> 10668 bytes static/js/Editor.js | 101 +++++++++++++++--------- static/js/Wiki.js | 112 ++++++++++++++++++++++++-- tools/initdb.py | 1 + tools/updatedb.py | 1 + view/Link_area.py | 14 +++- 13 files changed, 438 insertions(+), 102 deletions(-) create mode 100644 static/html/upgrade.html create mode 100644 static/images/check.png create mode 100644 static/images/check.xcf diff --git a/config/Common.py b/config/Common.py index aaf46dc..f393618 100644 --- a/config/Common.py +++ b/config/Common.py @@ -25,16 +25,24 @@ settings = { "luminotes.support_email": "", "luminotes.rate_plans": [ { - "name": "basic", + "name": "free", "storage_quota_bytes": 30 * MEGABYTE, + "notebook_sharing": False, + }, + { + "name": "basic", + "storage_quota_bytes": 250 * MEGABYTE, + "notebook_sharing": True, }, { "name": "standard", - "storage_quota_bytes": 100 * MEGABYTE, + "storage_quota_bytes": 500 * MEGABYTE, + "notebook_sharing": True, }, { - "name": "professional", - "storage_quota_bytes": 300 * MEGABYTE, + "name": "premium", + "storage_quota_bytes": 1000 * MEGABYTE, + "notebook_sharing": True, }, ], }, diff --git a/controller/Users.py b/controller/Users.py index e68cc3b..0e746be 100644 --- a/controller/Users.py +++ b/controller/Users.py @@ -17,8 +17,8 @@ from view.Redeem_reset_note import Redeem_reset_note USERNAME_PATTERN = re.compile( "^[a-zA-Z0-9]+$" ) -EMAIL_ADDRESS_PATTERN = re.compile( "^[\w.+]+@\w+(\.\w+)+$" ) -EMBEDDED_EMAIL_ADDRESS_PATTERN = re.compile( "(?:^|[\s,<])([\w.+]+@\w+(?:\.\w+)+)(?:[\s,>]|$)" ) +EMAIL_ADDRESS_PATTERN = re.compile( "^[\w.%+-]+@[\w-]+(\.[\w-]+)+$" ) +EMBEDDED_EMAIL_ADDRESS_PATTERN = re.compile( "(?:^|[\s,<])([\w.%+-]+@[\w-]+(?:\.[\w-]+)+)(?:[\s,>]|$)" ) WHITESPACE_OR_COMMA_PATTERN = re.compile( "[\s,]" ) @@ -508,9 +508,9 @@ class Users( object ): # create an email message with a unique link message = Message.Message() - message[ u"from" ] = u"Luminotes support <%s>" % self.__support_email - message[ u"to" ] = email_address - message[ u"subject" ] = u"Luminotes password reset" + message[ u"From" ] = u"Luminotes support <%s>" % self.__support_email + message[ u"To" ] = email_address + message[ u"Subject" ] = u"Luminotes password reset" message.set_payload( u"Someone has requested a password reset for a Luminotes user with your email\n" + u"address. If this someone is you, please visit the following link for a\n" + @@ -523,7 +523,7 @@ class Users( object ): # send the message out through localhost's smtp server server = smtplib.SMTP() server.connect() - server.sendmail( message[ u"from" ], [ email_address ], message.as_string() ) + server.sendmail( message[ u"From" ], [ email_address ], message.as_string() ) server.quit() return dict( @@ -655,12 +655,11 @@ class Users( object ): @validate( notebook_id = Valid_id(), email_addresses = unicode, - read_write = Valid_bool(), - owner = Valid_bool(), + access = Valid_string(), invite_button = unicode, user_id = Valid_id( none_okay = True ), ) - def send_invites( self, notebook_id, email_addresses, read_write, owner, invite_button, user_id = None ): + def send_invites( self, notebook_id, email_addresses, access, invite_button, user_id = None ): """ Send notebook invitations to the given email addresses. @@ -668,10 +667,8 @@ class Users( object ): @param notebook_id: id of the notebook that the invitation is for @type email_addresses: unicode @param email_addresses: a string containing whitespace- or comma-separated email addresses - @type read_write: bool - @param read_write: whether the invitation is for read-write access - @type owner: bool - @param owner: whether the invitation is for owner-level access + @type access: unicode + @param access: type of access to grant, either "collaborator", "viewer", or "owner" @type invite_button: unicode @param invite_button: ignored @type user_id: unicode @@ -683,10 +680,22 @@ class Users( object ): @raise Access_error: user_id doesn't have owner-level notebook access to send an invite """ if len( email_addresses ) < 5: - raise Invite_error( u"Please enter at least one email valid address." ) + raise Invite_error( u"Please enter at least one valid email address." ) if len( email_addresses ) > 5000: raise Invite_error( u"Please enter fewer email addresses." ) + if access == u"collaborator": + read_write = True + owner = False + elif access == u"viewer": + read_write = False + owner = False + elif access == u"owner": + read_write = True + owner = True + else: + raise Access_error() + if not self.check_access( user_id, notebook_id, read_write = True, owner = True ): raise Access_error() @@ -729,11 +738,11 @@ class Users( object ): # create an email message with a unique invitation link notebook_name = notebook.name.strip().replace( "\n", " " ).replace( "\r", " " ) message = Message.Message() - message[ u"from" ] = user.email_address or u"Luminotes personal wiki <%s>" % self.__support_email - if not user.email_address: - message[ u"sender" ] = u"Luminotes personal wiki <%s>" % self.__support_email - message[ u"to" ] = email_address - message[ u"subject" ] = notebook_name + message[ u"From" ] = user.email_address or u"Luminotes personal wiki <%s>" % self.__support_email + if user.email_address: + message[ u"Sender" ] = u"Luminotes personal wiki <%s>" % self.__support_email + message[ u"To" ] = email_address + message[ u"Subject" ] = notebook_name message.set_payload( u"I've shared a wiki with you called \"%s\"\n" % notebook_name + u"Please visit the following link to view it online:\n\n" + @@ -743,7 +752,7 @@ class Users( object ): # send the message out through localhost's smtp server server = smtplib.SMTP() server.connect() - server.sendmail( message[ u"from" ], [ email_address ], message.as_string() ) + server.sendmail( message[ u"From" ], [ email_address ], message.as_string() ) server.quit() self.__database.commit() diff --git a/controller/test/Test_users.py b/controller/test/Test_users.py index 41a3ced..f87c23d 100644 --- a/controller/test/Test_users.py +++ b/controller/test/Test_users.py @@ -23,13 +23,13 @@ class Test_users( Test_controller ): self.username = u"mulder" self.password = u"trustno1" - self.email_address = u"outthere@example.com" + self.email_address = u"out-there@example.com" self.new_username = u"reynolds" self.new_password = u"shiny" self.new_email_address = u"capn@example.com" self.username2 = u"scully" self.password2 = u"trustsome1" - self.email_address2 = u"outthere@example.com" + self.email_address2 = u"out-there@example.com" self.user = None self.user2 = None self.anonymous = None @@ -769,8 +769,7 @@ class Test_users( Test_controller ): result = self.http_post( "/users/send_invites", dict( notebook_id = self.notebooks[ 0 ].object_id, email_addresses = email_addresses, - read_write = False, - owner = False, + access = u"viewer", invite_button = u"send invites", ), session_id = self.session_id ) session_id = result[ u"session_id" ] @@ -783,7 +782,100 @@ class Test_users( Test_controller ): assert self.email_address in from_address assert to_addresses == email_addresses_list assert self.notebooks[ 0 ].name in message - assert self.INVITE_LINK_PATTERN.search( message ) + matches = self.INVITE_LINK_PATTERN.search( message ) + invite_id = matches.group( 2 ) + assert invite_id + + # assert that the invite has the read_write / owner flags set appropriately + invite_list = self.database.objects.get( invite_id ) + assert invite_list + assert len( invite_list ) == 1 + invite = invite_list[ -1 ] + assert invite + assert invite.read_write is False + assert invite.owner is False + + def test_send_invites_collaborator( self ): + # trick send_invites() into using a fake SMTP server + Stub_smtp.reset() + smtplib.SMTP = Stub_smtp + self.login() + + self.user.rate_plan = 1 + self.database.save( self.user ) + + email_addresses_list = [ u"foo@example.com" ] + email_addresses = email_addresses_list[ 0 ] + + result = self.http_post( "/users/send_invites", dict( + notebook_id = self.notebooks[ 0 ].object_id, + email_addresses = email_addresses, + access = u"collaborator", + invite_button = u"send invites", + ), session_id = self.session_id ) + session_id = result[ u"session_id" ] + + assert u"An invitation has been sent." in result[ u"message" ] + assert smtplib.SMTP.connected == False + assert len( smtplib.SMTP.emails ) == 1 + + ( from_address, to_addresses, message ) = smtplib.SMTP.emails[ 0 ] + assert self.email_address in from_address + assert to_addresses == email_addresses_list + assert self.notebooks[ 0 ].name in message + matches = self.INVITE_LINK_PATTERN.search( message ) + invite_id = matches.group( 2 ) + assert invite_id + + # assert that the invite has the read_write / owner flags set appropriately + invite_list = self.database.objects.get( invite_id ) + assert invite_list + assert len( invite_list ) == 1 + invite = invite_list[ -1 ] + assert invite + assert invite.read_write is True + assert invite.owner is False + + def test_send_invites_owner( self ): + # trick send_invites() into using a fake SMTP server + Stub_smtp.reset() + smtplib.SMTP = Stub_smtp + self.login() + + self.user.rate_plan = 1 + self.database.save( self.user ) + + email_addresses_list = [ u"foo@example.com" ] + email_addresses = email_addresses_list[ 0 ] + + result = self.http_post( "/users/send_invites", dict( + notebook_id = self.notebooks[ 0 ].object_id, + email_addresses = email_addresses, + access = u"owner", + invite_button = u"send invites", + ), session_id = self.session_id ) + session_id = result[ u"session_id" ] + + assert u"An invitation has been sent." in result[ u"message" ] + assert smtplib.SMTP.connected == False + assert len( smtplib.SMTP.emails ) == 1 + + ( from_address, to_addresses, message ) = smtplib.SMTP.emails[ 0 ] + assert self.email_address in from_address + assert to_addresses == email_addresses_list + assert self.notebooks[ 0 ].name in message + matches = self.INVITE_LINK_PATTERN.search( message ) + invite_id = matches.group( 2 ) + assert invite_id + + # assert that the invite has the read_write / owner flags set appropriately + invite_list = self.database.objects.get( invite_id ) + assert invite_list + assert len( invite_list ) == 1 + invite = invite_list[ -1 ] + assert invite + assert invite.read_write is True + assert invite.owner is True def test_send_invites_multiple( self ): Stub_smtp.reset() @@ -800,8 +892,7 @@ class Test_users( Test_controller ): result = self.http_post( "/users/send_invites", dict( notebook_id = self.notebooks[ 0 ].object_id, email_addresses = email_addresses, - read_write = False, - owner = False, + access = u"viewer", invite_button = u"send invites", ), session_id = self.session_id ) session_id = result[ u"session_id" ] @@ -834,8 +925,7 @@ class Test_users( Test_controller ): result = self.http_post( "/users/send_invites", dict( notebook_id = self.notebooks[ 0 ].object_id, email_addresses = email_addresses, - read_write = False, - owner = False, + access = u"viewer", invite_button = u"send invites", ), session_id = self.session_id ) session_id = result[ u"session_id" ] @@ -868,8 +958,7 @@ class Test_users( Test_controller ): self.http_post( "/users/send_invites", dict( notebook_id = self.notebooks[ 0 ].object_id, email_addresses = email_addresses, - read_write = False, - owner = False, + access = u"viewer", invite_button = u"send invites", ), session_id = self.session_id ) @@ -881,8 +970,7 @@ class Test_users( Test_controller ): self.http_post( "/users/send_invites", dict( notebook_id = self.notebooks[ 0 ].object_id, email_addresses = email_addresses, - read_write = True, - owner = True, + access = u"owner", invite_button = u"send invites", ), session_id = self.session_id ) @@ -922,8 +1010,7 @@ class Test_users( Test_controller ): result = self.http_post( "/users/send_invites", dict( notebook_id = self.notebooks[ 0 ].object_id, email_addresses = email_addresses, - read_write = False, - owner = False, + access = u"viewer", invite_button = u"send invites", ), session_id = self.session_id ) session_id = result[ u"session_id" ] @@ -951,8 +1038,7 @@ class Test_users( Test_controller ): result = self.http_post( "/users/send_invites", dict( notebook_id = self.notebooks[ 0 ].object_id, email_addresses = email_addresses, - read_write = False, - owner = False, + access = u"viewer", invite_button = u"send invites", ), session_id = self.session_id ) session_id = result[ u"session_id" ] @@ -974,8 +1060,7 @@ class Test_users( Test_controller ): result = self.http_post( "/users/send_invites", dict( notebook_id = self.notebooks[ 0 ].object_id, email_addresses = email_addresses, - read_write = False, - owner = False, + access = u"viewer", invite_button = u"send invites", ), session_id = self.session_id ) session_id = result[ u"session_id" ] @@ -996,8 +1081,7 @@ class Test_users( Test_controller ): result = self.http_post( "/users/send_invites", dict( notebook_id = self.notebooks[ 0 ].object_id, email_addresses = email_addresses, - read_write = False, - owner = False, + access = u"viewer", invite_button = u"send invites", ), session_id = self.session_id ) session_id = result[ u"session_id" ] @@ -1018,8 +1102,7 @@ class Test_users( Test_controller ): result = self.http_post( "/users/send_invites", dict( notebook_id = self.notebooks[ 0 ].object_id, email_addresses = email_addresses, - read_write = False, - owner = False, + access = u"viewer", invite_button = u"send invites", ), session_id = self.session_id ) session_id = result[ u"session_id" ] @@ -1041,8 +1124,7 @@ class Test_users( Test_controller ): result = self.http_post( "/users/send_invites", dict( notebook_id = self.notebooks[ 0 ].object_id, email_addresses = email_addresses, - read_write = False, - owner = False, + access = u"viewer", invite_button = u"send invites", ), session_id = self.session_id ) session_id = result[ u"session_id" ] @@ -1066,8 +1148,7 @@ class Test_users( Test_controller ): result = self.http_post( "/users/send_invites", dict( notebook_id = self.notebooks[ 0 ].object_id, email_addresses = email_addresses, - read_write = False, - owner = False, + access = u"viewer", invite_button = u"send invites", ), session_id = self.session_id ) session_id = result[ u"session_id" ] @@ -1091,8 +1172,7 @@ class Test_users( Test_controller ): result = self.http_post( "/users/send_invites", dict( notebook_id = self.notebooks[ 0 ].object_id, email_addresses = email_addresses, - read_write = False, - owner = False, + access = u"viewer", invite_button = u"send invites", ), session_id = self.session_id ) session_id = result[ u"session_id" ] @@ -1111,8 +1191,7 @@ class Test_users( Test_controller ): result = self.http_post( "/users/send_invites", dict( notebook_id = self.notebooks[ 0 ].object_id, email_addresses = email_addresses, - read_write = False, - owner = False, + access = u"viewer", invite_button = u"send invites", ), session_id = self.session_id ) session_id = result[ u"session_id" ] @@ -1132,8 +1211,7 @@ class Test_users( Test_controller ): result = self.http_post( "/users/send_invites", dict( notebook_id = unknown_notebook_id, email_addresses = email_addresses, - read_write = False, - owner = False, + access = u"viewer", invite_button = u"send invites", ), session_id = self.session_id ) session_id = result[ u"session_id" ] diff --git a/model/Invite.py b/model/Invite.py index dd4d86f..02d8bce 100644 --- a/model/Invite.py +++ b/model/Invite.py @@ -86,9 +86,9 @@ class Invite( Persistent ): def sql_update( self ): return "update invite set revision = %s, from_user_id = %s, notebook_id = %s, email_address = %s, read_write = %s, owner = %s, redeemed_user_id = %s where id = %s;" % \ - ( quote( self.object_id ), quote( self.revision ), quote( self.__from_user_id ), quote( self.__notebook_id ), + ( quote( self.revision ), quote( self.__from_user_id ), quote( self.__notebook_id ), quote( self.__email_address ), quote( self.__read_write and "t" or "f" ), quote( self.__owner and "t" or "f" ), - quote( self.__redeemed_user_id ) ) + quote( self.__redeemed_user_id ), quote( self.object_id ) ) def sql_load_similar( self ): # select unredeemed invitations with the same from_user_id, notebook_id, and email_address as this invitation diff --git a/static/css/note.css b/static/css/note.css index b34ca58..474ddc0 100644 --- a/static/css/note.css +++ b/static/css/note.css @@ -79,6 +79,13 @@ h3 { border: #999999 1px solid; } +.textarea_field { + margin-top: 0.25em; + padding: 0.25em; + border: #999999 1px solid; + overflow: auto; +} + ul { list-style-type: disc; } @@ -99,3 +106,49 @@ ol li { padding-top: 0.5em; font-size: 90%; } + +.radio_link { + color: #000000; + text-decoration: none; +} + +.radio_link:hover { + color: #ff6600; + text-decoration: none; +} + +#access_table td { + padding-right: 1em; +} + +#upgrade_table { + border-collapse: collapse; + border: 1px solid #999999; +} + +#upgrade_table .plan_name { + width: 16%; + text-align: center; + background-color: #d0e0f0; +} + +#upgrade_table .feature_name { + width: 36%; + text-align: left; + background-color: #fafafa; +} + +#upgrade_table .price_text { + color: #ff6600; +} + +#upgrade_table .month_text { + padding-top: 0.5em; + font-size: 75%; +} + +#upgrade_table td { + text-align: center; + background-color: #fafafa; + padding: 0.5em; +} diff --git a/static/html/upgrade.html b/static/html/upgrade.html new file mode 100644 index 0000000..a7d2cc1 --- /dev/null +++ b/static/html/upgrade.html @@ -0,0 +1,45 @@ +

upgrade

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 FreeBasic
$5/month
Standard
$9/month
Premium
$14/month
included storage space30 MB250 MB500 MB1000 MB
unlimited wiki notebooks
friendly email support
multi-user collaboration 
wiki access control 
diff --git a/static/images/check.png b/static/images/check.png new file mode 100644 index 0000000000000000000000000000000000000000..d2033a6d8b06b881dc8fa4e84fa76cae34db7854 GIT binary patch literal 605 zcmV-j0;2tiP)+5;8Al4sHMd03CEi zSad^gZEa<4bO1wgWnpw>WFU8GbZ8({Xk{QrNlj4iWF>9@00F^CL_t(I%dM0@NE=}o z#($SykT;NT|i;vxtRPC7Zc2)ZQTbT|faa=Jr=KtV|-{~!Td zqy%zm3Lzjwa`$2H?NBHjR(ogr!8g4xzwdjW=lMQODJA@eb=W>6XXHrzE9m(4nAOWw z@@Ag=%RKsXL7{|f);DSR4c1<*k=3&R&jYqpi{-D&Wb_Qhcg098@yPXVovD*4Qd)|Q z*BcM&S`t${-(#*eM@UGyP-dWK;6YEKk;^w_E;<)DW6mF8v4Nq4Y0%i)iR|J$9NqO!iGOk{yXSI$9lN$QaRO>?O)UTqL=QEE&%6u=hzJ!fc5%1 zm3D;{YlVrviFnQ_rNj?>nLV8q90UhRyJ@k%*fN1xu6f5mQdmnCZ{@yGEKnT8JD8CaV900000NkvXXu0mjf9~K9* literal 0 HcmV?d00001 diff --git a/static/images/check.xcf b/static/images/check.xcf new file mode 100644 index 0000000000000000000000000000000000000000..b8d119c1823cd5b1caab02879ef42be700e91e26 GIT binary patch literal 10668 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 notes" + list_holder.innerHTML, undefined, undefined, undefined, false, true, true, getElement( "notes_top" ) ); } +Wiki.prototype.share_notebook = function () { + this.clear_messages(); + this.clear_pulldowns(); + + var share_notebook_frame = getElement( "note_share_notebook" ); + if ( share_notebook_frame ) { + share_notebook_frame.editor.highlight(); + return; + } + + if ( !this.rate_plan.notebook_sharing ) { + this.display_message( + "If you'd like to share your notebook, please ", + [ createDOM( "a", { "href": "/upgrade", "target": "_new" }, "upgrade" ), + " your account first." ] + ); + return; + } + + var collaborators_link = createDOM( "a", + { "href": "#", "id": "collaborators_link", "class": "radio_link", "title": "Collaborators may view and edit this notebook." }, + "collaborators" + ); + var viewers_link = createDOM( "a", + { "href": "#", "id": "viewers_link", "class": "radio_link", "title": "Viewers may only view this notebook." }, + "viewers" + ); + var owners_link = createDOM( "a", + { "href": "#", "id": "owners_link", "class": "radio_link", "title": "Owners may view, edit, rename, delete, and invite people to this notebook." }, + "owners" + ); + + var collaborators_radio = createDOM( "input", + { "type": "radio", "id": "collaborators_radio", "name": "access", "value": "collaborator", "checked": "true" } + ); + var viewers_radio = createDOM( "input", + { "type": "radio", "id": "viewers_radio", "name": "access", "value": "viewer" } + ); + var owners_radio = createDOM( "input", + { "type": "radio", "id": "owners_radio", "name": "access", "value": "owner" } + ) + + var div = createDOM( "div", {}, + createDOM( "form", { "id": "invite_form" }, + createDOM( "input", { "type": "hidden", "name": "notebook_id", "value": this.notebook_id } ), + createDOM( "p", {}, + createDOM( "b", {}, "people to invite" ), + createDOM( "br", {} ), + createDOM( "textarea", + { "name": "email_addresses", "class": "textarea_field", "cols": "40", "rows": "4", "wrap": "off" } + ) + ), + createDOM( "p", {}, "Please separate email addresses with commas, spaces, or the enter key." ), + createDOM( "p", {}, + createDOM( "p", {}, "Invite these people as:" ), + createDOM( "table" , { "id": "access_table" }, + createDOM( "tr", {}, + createDOM( "td", {}, collaborators_radio, collaborators_link ), + createDOM( "td", {}, viewers_radio, viewers_link ), + createDOM( "td", {}, owners_radio, owners_link ) + ) + ) + ), + createDOM( "p", {}, + createDOM( "input", + { "type": "submit", "name": "invite_button", "id": "invite_button", "class": "button", "value": "send invites" } + ) + ) + ) + ); + + this.create_editor( "share_notebook", "

share this notebook

" + div.innerHTML, undefined, undefined, undefined, false, true, true, getElement( "notes_top" ) ); +} + Wiki.prototype.display_message = function ( text, nodes, position_after ) { this.clear_messages(); this.clear_pulldowns(); @@ -1432,7 +1526,7 @@ Wiki.prototype.remove_all_notes_link = function ( note_id ) { Wiki.prototype.add_all_notes_link = function ( note_id, note_title ) { if ( !this.all_notes_editor ) return; - if ( note_title == "all notes" || note_title == "search results" ) return; + if ( note_title == "all notes" || note_title == "search results" || note_title == "share this notebook" ) return; if ( !note_title || note_title.length == 0 ) note_title = "untitled note"; @@ -1709,7 +1803,7 @@ function Options_pulldown( wiki, notebook_id, invoker, editor ) { this.invoker = invoker; this.editor = editor; this.startup_checkbox = createDOM( "input", { "type": "checkbox", "class": "pulldown_checkbox" } ); - this.startup_toggle = createDOM( "a", { "href": "", "class": "pulldown_link", "title": "Display this note whenever the notebook is loaded." }, + this.startup_toggle = createDOM( "a", { "href": "#", "class": "pulldown_link", "title": "Display this note whenever the notebook is loaded." }, "show on startup" ); @@ -1840,6 +1934,12 @@ function Link_pulldown( wiki, notebook_id, invoker, editor, link ) { return; } + if ( title == "share this notebook" ) { + this.title_field.value = title; + this.display_summary( title, "share this notebook with others" ); + return; + } + this.invoker.invoke( "/notebooks/load_note_by_title", "GET", { "notebook_id": this.notebook_id, diff --git a/tools/initdb.py b/tools/initdb.py index 49844ba..453b532 100644 --- a/tools/initdb.py +++ b/tools/initdb.py @@ -24,6 +24,7 @@ class Initializer( object ): ( u"password reset.html", False ), ( u"advanced browser features.html", False ), ( u"supported browsers.html", False ), + ( u"upgrade.html", False ), ] def __init__( self, database, nuke = False ): diff --git a/tools/updatedb.py b/tools/updatedb.py index 26afb73..3fa5e97 100755 --- a/tools/updatedb.py +++ b/tools/updatedb.py @@ -24,6 +24,7 @@ class Updater( object ): ( u"password reset.html", False ), ( u"advanced browser features.html", False ), ( u"supported browsers.html", False ), + ( u"upgrade.html", False ), ] def __init__( self, database, navigation_note_id = None ): diff --git a/view/Link_area.py b/view/Link_area.py index d4a8563..75f09ca 100644 --- a/view/Link_area.py +++ b/view/Link_area.py @@ -45,7 +45,7 @@ class Link_area( Div ): ) or None, notebook.read_write and Span( - ( notebook.name != u"trash" ) and Div( + ( notebook.owner and notebook.name != u"trash" ) and Div( A( u"rename notebook", href = u"#", @@ -55,7 +55,7 @@ class Link_area( Div ): class_ = u"link_area_item", ) or None, - ( notebook.name != u"trash" ) and Div( + ( notebook.owner and notebook.name != u"trash" ) and Div( A( u"delete notebook", href = u"#", @@ -75,6 +75,16 @@ class Link_area( Div ): class_ = u"link_area_item", ), + ( notebook.owner ) and Div( + A( + u"share", + href = u"#", + id = u"share_notebook_link", + title = u"Share this notebook with others.", + ), + class_ = u"link_area_item", + ) or None, + notebook.trash_id and Div( A( u"trash",