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 @@ +
+ | Free | +Basic $5/month |
+ Standard $9/month |
+ Premium $14/month |
+
---|---|---|---|---|
included storage space | +30 MB | +250 MB | +500 MB | +1000 MB | +
unlimited wiki notebooks | ++ | + | + | + |
friendly email support | ++ | + | + | + |
multi-user collaboration | ++ | + | + | + |
wiki access control | ++ | + | + | + |