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 0000000..d2033a6 Binary files /dev/null and b/static/images/check.png differ diff --git a/static/images/check.xcf b/static/images/check.xcf new file mode 100644 index 0000000..b8d119c Binary files /dev/null and b/static/images/check.xcf differ diff --git a/static/js/Editor.js b/static/js/Editor.js index 58f53b6..d4d9362 100644 --- a/static/js/Editor.js +++ b/static/js/Editor.js @@ -136,48 +136,79 @@ Editor.prototype.finish_init = function () { if ( this.read_write ) { connect( this.document, "onkeydown", function ( event ) { self.key_pressed( event ); } ); connect( this.document, "onkeyup", function ( event ) { self.key_released( event ); } ); - connect( this.document, "onblur", function ( event ) { self.blurred( event ); } ); - connect( this.document, "onfocus", function ( event ) { self.focused( event ); } ); - connect( this.document.body, "onblur", function ( event ) { self.blurred( event ); } ); - connect( this.document.body, "onfocus", function ( event ) { self.focused( event ); } ); - connect( this.iframe.contentWindow, "onblur", function ( event ) { self.blurred( event ); } ); - connect( this.iframe.contentWindow, "onfocus", function ( event ) { self.focused( event ); } ); } + connect( this.document, "onblur", function ( event ) { self.blurred( event ); } ); + connect( this.document, "onfocus", function ( event ) { self.focused( event ); } ); + connect( this.document.body, "onblur", function ( event ) { self.blurred( event ); } ); + connect( this.document.body, "onfocus", function ( event ) { self.focused( event ); } ); + connect( this.iframe.contentWindow, "onblur", function ( event ) { self.blurred( event ); } ); + connect( this.iframe.contentWindow, "onfocus", function ( event ) { self.focused( event ); } ); - connect( this.document, "onclick", function ( event ) { self.mouse_clicked( event ); } ); + // special case: don't handle mouse click for share_notebook magic note, so radio button links + // work as intended + if ( this.id != "share_notebook" ) + connect( this.document, "onclick", function ( event ) { self.mouse_clicked( event ); } ); // special-case: connect any submit buttons within the contents of this note - var signup_button = withDocument( this.document, function () { return getElement( "signup_button" ); } ); - if ( signup_button ) { - var signup_form = withDocument( this.document, function () { return getElement( "signup_form" ); } ); - connect( signup_button, "onclick", function ( event ) { - signal( self, "submit_form", "/users/signup", signup_form ); event.stop(); - } ); - } + withDocument( this.document, function () { + var signup_button = getElement( "signup_button" ); + if ( signup_button ) { + var signup_form = getElement( "signup_form" ); + connect( signup_button, "onclick", function ( event ) { + signal( self, "submit_form", "/users/signup", signup_form ); event.stop(); + } ); + } - var login_button = withDocument( this.document, function () { return getElement( "login_button" ); } ); - if ( login_button ) { - var login_form = withDocument( this.document, function () { return getElement( "login_form" ); } ); - connect( login_button, "onclick", function ( event ) { - signal( self, "submit_form", "/users/login", login_form ); event.stop(); - } ); - } + var login_button = getElement( "login_button" ); + if ( login_button ) { + var login_form = getElement( "login_form" ); + connect( login_button, "onclick", function ( event ) { + signal( self, "submit_form", "/users/login", login_form ); event.stop(); + } ); + } - var send_reset_button = withDocument( this.document, function () { return getElement( "send_reset_button" ); } ); - if ( send_reset_button ) { - var send_reset_form = withDocument( this.document, function () { return getElement( "send_reset_form" ); } ); - connect( send_reset_button, "onclick", function ( event ) { - signal( self, "submit_form", "/users/send_reset", send_reset_form ); event.stop(); - } ); - } + var send_reset_button = getElement( "send_reset_button" ); + if ( send_reset_button ) { + var send_reset_form = getElement( "send_reset_form" ); + connect( send_reset_button, "onclick", function ( event ) { + signal( self, "submit_form", "/users/send_reset", send_reset_form ); event.stop(); + } ); + } - var reset_button = withDocument( this.document, function () { return getElement( "reset_button" ); } ); - if ( reset_button ) { - var reset_form = withDocument( this.document, function () { return getElement( "reset_form" ); } ); - connect( reset_button, "onclick", function ( event ) { - signal( self, "submit_form", "/users/reset_password", reset_form ); event.stop(); - } ); - } + var reset_button = getElement( "reset_button" ); + if ( reset_button ) { + var reset_form = getElement( "reset_form" ); + connect( reset_button, "onclick", function ( event ) { + signal( self, "submit_form", "/users/reset_password", reset_form ); event.stop(); + } ); + } + + var invite_button = getElement( "invite_button" ); + if ( invite_button ) { + var collaborators_radio = getElement( "collaborators_radio" ); + connect( "collaborators_link", "onclick", function ( event ) { + collaborators_radio.checked = true; + event.stop(); + } ); + + var viewers_radio = getElement( "viewers_radio" ); + connect( "viewers_link", "onclick", function ( event ) { + viewers_radio.checked = true; + event.stop(); + } ); + + var owners_radio = getElement( "owners_radio" ); + connect( "owners_link", "onclick", function ( event ) { + owners_radio.checked = true; + event.stop(); + } ); + + var invite_form = getElement( "invite_form" ); + connect( invite_button, "onclick", function ( event ) { + signal( self, "submit_form", "/users/send_invites", invite_form ); event.stop(); + } ); + } + } ); // browsers such as Firefox, but not Opera if ( this.iframe.contentDocument && !/Opera/.test( navigator.userAgent ) && this.read_write ) diff --git a/static/js/Wiki.js b/static/js/Wiki.js index 653ed2a..f29e29f 100644 --- a/static/js/Wiki.js +++ b/static/js/Wiki.js @@ -146,8 +146,8 @@ Wiki.prototype.display_storage_usage = function( storage_bytes ) { Wiki.prototype.populate = function ( startup_notes, current_notes, note_read_write, skip_empty_message ) { var self = this; - // if this is the trash, display a list of all deleted notebooks - if ( this.notebook.name == "trash" ) { + // if this is the trash and the user has owner-level access, then display a list of all deleted notebooks + if ( this.notebook.owner && this.notebook.name == "trash" ) { var heading_shown = false; var deleted_notebooks = getElement( "deleted_notebooks" ); @@ -312,6 +312,14 @@ Wiki.prototype.populate = function ( startup_notes, current_notes, note_read_wri event.stop(); } ); } + + var share_notebook_link = getElement( "share_notebook_link" ); + if ( share_notebook_link ) { + connect( share_notebook_link, "onclick", function ( event ) { + self.load_editor( "share this notebook", "null", null, null, getElement( "notes_top" ) ); + event.stop(); + } ); + } } Wiki.prototype.background_clicked = function ( event ) { @@ -413,6 +421,16 @@ Wiki.prototype.load_editor = function ( note_title, note_id, revision, link, pos this.display_search_results(); return; } + if ( note_title == "share this notebook" ) { + var editor = this.open_editors[ note_title ]; + if ( editor ) { + editor.highlight(); + return; + } + + this.share_notebook(); + return; + } // but if the note corresponding to the link's title is already open, highlight it and bail if ( !revision ) { @@ -457,7 +475,7 @@ Wiki.prototype.resolve_link = function ( note_title, link, callback ) { if ( link && link.target ) link.removeAttribute( "target" ); - if ( note_title == "all notes" || note_title == "search results" ) { + if ( note_title == "all notes" || note_title == "search results" || note_title == "share this notebook" ) { link.href = "/notebooks/" + this.notebook_id + "?" + queryString( [ "title", "note_id" ], [ note_title, "null" ] @@ -465,8 +483,10 @@ Wiki.prototype.resolve_link = function ( note_title, link, callback ) { if ( callback ) { if ( note_title == "all notes" ) callback( "list of all notes in this notebook" ); - else + if ( note_title == "search results" ) callback( "current search results" ); + else + callback( "share this notebook with others" ); } return; } @@ -1220,6 +1240,80 @@ Wiki.prototype.display_all_notes_list = function ( result ) { this.all_notes_editor = this.create_editor( "all_notes", "

all 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",