diff --git a/controller/Root.py b/controller/Root.py index e1dd779..a723a14 100644 --- a/controller/Root.py +++ b/controller/Root.py @@ -56,9 +56,10 @@ class Root( object ): note_title = unicode, invite_id = Valid_id( none_okay = True ), after_login = Valid_string( min = 0, max = 100 ), + plan = Valid_int( none_okay = True ), user_id = Valid_id( none_okay = True ), ) - def default( self, note_title, invite_id = None, after_login = None, user_id = None ): + def default( self, note_title, invite_id = None, after_login = None, plan = None, user_id = None ): """ Convenience method for accessing a note in the main notebook by name rather than by note id. @@ -68,6 +69,8 @@ class Root( object ): @param invite_id: id of the invite used to get to this note (optional) @type after_login: unicode @param after_login: URL to redirect to after login (optional, must start with "/") + @type plan: int + @param plan: rate plan index (optional, defaults to None) @rtype: unicode @return: rendered HTML page """ @@ -81,6 +84,8 @@ class Root( object ): return dict( redirect = u"%s/%s?invite_id=%s" % ( https_url, note_title, invite_id ) ) if after_login: return dict( redirect = u"%s/%s?after_login=%s" % ( https_url, note_title, after_login ) ) + if plan: + return dict( redirect = u"%s/%s?plan=%s" % ( https_url, note_title, plan ) ) else: return dict( redirect = u"%s/%s" % ( https_url, note_title ) ) @@ -100,6 +105,8 @@ class Root( object ): result[ "invite_id" ] = invite_id if after_login and after_login.startswith( u"/" ): result[ "after_login" ] = after_login + if plan: + result[ "signup_plan" ] = plan return result diff --git a/controller/Users.py b/controller/Users.py index 8d74a6d..1f50b82 100644 --- a/controller/Users.py +++ b/controller/Users.py @@ -10,7 +10,7 @@ from model.Note import Note from model.Password_reset import Password_reset from model.Invite import Invite from Expose import expose -from Validate import validate, Valid_string, Valid_bool, Validation_error +from Validate import validate, Valid_string, Valid_bool, Valid_int, Validation_error from Database import Valid_id, end_transaction from Expire import strongly_expire from view.Json import Json @@ -21,6 +21,7 @@ from view.Blank_page import Blank_page from view.Thanks_note import Thanks_note from view.Thanks_error_note import Thanks_error_note from view.Processing_note import Processing_note +from view.Form_submit_page import Form_submit_page USERNAME_PATTERN = re.compile( "^[a-zA-Z0-9]+$" ) @@ -205,8 +206,9 @@ class Users( object ): email_address = ( Valid_string( min = 0, max = 60 ) ), signup_button = unicode, invite_id = Valid_id( none_okay = True ), + rate_plan = Valid_int( none_okay = True ), ) - def signup( self, username, password, password_repeat, email_address, signup_button, invite_id = None ): + def signup( self, username, password, password_repeat, email_address, signup_button, invite_id = None, rate_plan = None ): """ Create a new User based on the given information. Start that user with their own Notebook and a "welcome to your wiki" Note. For convenience, login the newly created user as well. @@ -223,6 +225,9 @@ class Users( object ): @param signup_button: ignored @type invite_id: unicode @param invite_id: id of invite to redeem upon signup (optional) + @type rate_plan: int + @param rate_plan: index of rate plan to signup for (optional). if greater than zero, redirect + to PayPal subscribe page after signup @rtype: json dict @return: { 'redirect': url, 'authenticated': userdict } @raise Signup_error: passwords don't match or the username is unavailable @@ -275,6 +280,9 @@ class Users( object ): self.convert_invite_to_access( invite, user_id ) redirect = u"/notebooks/%s" % invite.notebook_id + # if there's a requested rate plan, then redirect to the PayPal subscribe page + elif rate_plan and rate_plan > 0: + redirect = u"/users/subscribe?rate_plan=%s" % rate_plan # otherwise, just redirect to the newly created notebook else: redirect = u"/notebooks/%s" % notebook.object_id @@ -284,6 +292,39 @@ class Users( object ): authenticated = user, ) + @expose( view = Form_submit_page ) + @grab_user_id + @validate( + rate_plan = Valid_int(), + user_id = Valid_id(), + ) + def subscribe( self, rate_plan, user_id ): + """ + Submit a subscription form to PayPal, allowing the user to subscribe to the given rate plan. + + @type rate_plan: int + @param rate_plan: index of rate plan to subscribe to + @type user_id: unicode + @param user_id: id of current logged-in user + @rtype: dict + @return: { 'form': subscription_form_html } + @raise Signup_error: invalid rate plan, no logged-in user, or missing subscribe button + """ + if rate_plan == 0 or rate_plan >= len( self.__rate_plans ): + raise Signup_error( u"The rate plan is invalid." ) + + plan = self.__rate_plans[ rate_plan ] + button = plan.get( u"button" ) + if not button or not button.strip(): + raise Signup_error( + u"Sorry, that rate plan is not configured for subscriptions. Please contact %s." % \ + ( self.__support_email or u"support" ) + ) + + return dict( + form = button % user_id, + ) + @expose() @end_transaction @grab_user_id diff --git a/controller/Validate.py b/controller/Validate.py index f6a4794..3030f46 100644 --- a/controller/Validate.py +++ b/controller/Validate.py @@ -142,12 +142,14 @@ class Valid_int( object ): """ Validator for an integer value. """ - def __init__( self, min = None, max = None ): + def __init__( self, min = None, max = None, none_okay = False ): self.min = min self.max = max self.message = None + self.__none_okay = none_okay def __call__( self, value ): + if self.__none_okay and value in ( None, "None", "" ): return None value = int( value ) if self.min is not None and value < self.min: diff --git a/controller/test/Test_root.py b/controller/test/Test_root.py index c0a8c7e..533941e 100644 --- a/controller/test/Test_root.py +++ b/controller/test/Test_root.py @@ -238,6 +238,21 @@ class Test_root( Test_controller ): assert result.get( u"after_login" ) is None assert result[ u"user" ].object_id == self.anonymous.object_id + def test_default_with_plan( self ): + plan = u"17" + + result = self.http_get( + "/my_note?plan=%s" % plan, + ) + + assert result + assert result[ u"notes" ] + assert len( result[ u"notes" ] ) == 1 + assert result[ u"notes" ][ 0 ].object_id == self.anon_note.object_id + assert result[ u"notebook" ].object_id == self.anon_notebook.object_id + assert result[ u"signup_plan" ] == 17 + assert result[ u"user" ].object_id == self.anonymous.object_id + def test_default_after_login( self ): self.login() diff --git a/controller/test/Test_users.py b/controller/test/Test_users.py index 044ec17..c345fe3 100644 --- a/controller/test/Test_users.py +++ b/controller/test/Test_users.py @@ -93,6 +93,18 @@ class Test_users( Test_controller ): assert result[ u"redirect" ].startswith( u"/notebooks/" ) + def test_signup_with_rate_plan( self ): + result = self.http_post( "/users/signup", dict( + username = self.new_username, + password = self.new_password, + password_repeat = self.new_password, + email_address = self.new_email_address, + signup_button = u"sign up", + rate_plan = u"2", + ) ) + + assert result[ u"redirect" ] == u"/users/subscribe?rate_plan=2" + def test_signup_without_email_address( self ): result = self.http_post( "/users/signup", dict( username = self.new_username, @@ -250,6 +262,62 @@ class Test_users( Test_controller ): assert rate_plan[ u"name" ] == u"super" assert rate_plan[ u"storage_quota_bytes" ] == 1337 * 10 + def test_current_after_signup_with_rate_plan( self ): + result = self.http_post( "/users/signup", dict( + username = self.new_username, + password = self.new_password, + password_repeat = self.new_password, + email_address = self.new_email_address, + signup_button = u"sign up", + rate_plan = u"2", + ) ) + session_id = result[ u"session_id" ] + + assert result[ u"redirect" ] == u"/users/subscribe?rate_plan=2" + + user = self.database.last_saved_obj + assert isinstance( user, User ) + result = cherrypy.root.users.current( user.object_id ) + + assert result[ u"user" ].object_id == user.object_id + assert result[ u"user" ].username == self.new_username + assert result[ u"user" ].email_address == self.new_email_address + + notebooks = result[ u"notebooks" ] + notebook = notebooks[ 0 ] + assert notebook.object_id + assert notebook.revision + assert notebook.name == u"my notebook" + assert notebook.trash_id + assert notebook.read_write == True + assert notebook.owner == True + assert notebook.rank == 0 + + notebook = notebooks[ 1 ] + assert notebook.object_id == notebooks[ 0 ].trash_id + assert notebook.revision + assert notebook.name == u"trash" + assert notebook.trash_id == None + assert notebook.read_write == True + assert notebook.owner == True + assert notebook.rank == None + + notebook = notebooks[ 2 ] + assert notebook.object_id == self.anon_notebook.object_id + assert notebook.revision == self.anon_notebook.revision + assert notebook.name == self.anon_notebook.name + assert notebook.trash_id == None + assert notebook.read_write == False + assert notebook.owner == False + assert notebook.rank == None + + assert result.get( u"login_url" ) is None + assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout" + + rate_plan = result[ u"rate_plan" ] + assert rate_plan[ u"name" ] == u"super" + assert rate_plan[ u"storage_quota_bytes" ] == 1337 * 10 + def test_signup_with_different_passwords( self ): result = self.http_post( "/users/signup", dict( username = self.new_username, @@ -261,6 +329,58 @@ class Test_users( Test_controller ): assert result[ u"error" ] + def test_subscribe( self ): + self.login() + + result = self.http_post( "/users/subscribe", dict( + rate_plan = u"1", + ), session_id = self.session_id ) + + form = result.get( u"form" ) + plan = self.settings[ u"global" ][ u"luminotes.rate_plans" ][ 1 ] + + assert form == plan[ u"button" ] % self.user.object_id + + def test_subscribe_with_free_rate_plan( self ): + self.login() + + result = self.http_post( "/users/subscribe", dict( + rate_plan = u"0", + ), session_id = self.session_id ) + + assert u"plan" in result[ u"error" ] + assert u"invalid" in result[ u"error" ] + + def test_subscribe_with_invalid_rate_plan( self ): + self.login() + + result = self.http_post( "/users/subscribe", dict( + rate_plan = u"17", + ), session_id = self.session_id ) + + assert u"plan" in result[ u"error" ] + assert u"invalid" in result[ u"error" ] + + def test_subscribe_without_login( self ): + result = self.http_post( "/users/subscribe", dict( + rate_plan = u"1", + ) ) + + assert u"user" in result[ u"error" ] + assert u"invalid" in result[ u"error" ] + + def test_subscribe_without_subscribe_button( self ): + self.login() + self.settings[ u"global" ][ u"luminotes.rate_plans" ][ 1 ][ u"button" ] = u" " + + result = self.http_post( "/users/subscribe", dict( + rate_plan = u"1", + ), session_id = self.session_id ) + + + print result + assert u"not configured" in result[ u"error" ] + def test_demo( self ): result = self.http_post( "/users/demo", dict() ) diff --git a/static/css/product.css b/static/css/product.css index 192e096..444937f 100644 --- a/static/css/product.css +++ b/static/css/product.css @@ -348,6 +348,7 @@ .upgrade_left_area { width: 400px; + margin-top: 1.5em; margin-bottom: 1em; } diff --git a/static/images/sign_up_button.png b/static/images/sign_up_button.png index c1c7250..dfe5557 100644 Binary files a/static/images/sign_up_button.png and b/static/images/sign_up_button.png differ diff --git a/static/images/sign_up_button.xcf b/static/images/sign_up_button.xcf index 54c2d7c..e1257f4 100644 Binary files a/static/images/sign_up_button.xcf and b/static/images/sign_up_button.xcf differ diff --git a/static/images/subscribe_button.png b/static/images/subscribe_button.png index 8b325d9..bd54850 100644 Binary files a/static/images/subscribe_button.png and b/static/images/subscribe_button.png differ diff --git a/static/images/subscribe_button.xcf b/static/images/subscribe_button.xcf index af0bb3e..304dc5a 100644 Binary files a/static/images/subscribe_button.xcf and b/static/images/subscribe_button.xcf differ diff --git a/static/images/unsubscribe_button.png b/static/images/unsubscribe_button.png index 2e9fa54..5e0bbb4 100644 Binary files a/static/images/unsubscribe_button.png and b/static/images/unsubscribe_button.png differ diff --git a/static/images/unsubscribe_button.xcf b/static/images/unsubscribe_button.xcf index 964dc5b..3567754 100644 Binary files a/static/images/unsubscribe_button.xcf and b/static/images/unsubscribe_button.xcf differ diff --git a/static/js/Wiki.js b/static/js/Wiki.js index 18d04e0..c6466a5 100644 --- a/static/js/Wiki.js +++ b/static/js/Wiki.js @@ -17,6 +17,7 @@ function Wiki( invoker ) { this.invites = evalJSON( getElement( "invites" ).value ); this.invite_id = getElement( "invite_id" ).value; this.after_login = getElement( "after_login" ).value; + this.signup_plan = getElement( "signup_plan" ).value; this.font_size = null; var total_notes_count_node = getElement( "total_notes_count" ); @@ -693,10 +694,12 @@ Wiki.prototype.create_editor = function ( id, note_text, deleted_from_id, revisi connect( editor, "invites_updated", function ( invites ) { self.invites = invites; self.share_notebook(); } ); connect( editor, "submit_form", function ( url, form, callback ) { var args = {} - if ( url == "/users/signup" || url == "/users/login" ) { + if ( url == "/users/signup" ) { args[ "invite_id" ] = self.invite_id; - if ( url == "/users/login" ) - args[ "after_login" ] = self.after_login; + args[ "rate_plan" ] = self.signup_plan; + } else if ( url == "/users/login" ) { + args[ "invite_id" ] = self.invite_id; + args[ "after_login" ] = self.after_login; } self.invoker.invoke( url, "POST", args, callback, form ); diff --git a/view/Form_submit_page.py b/view/Form_submit_page.py new file mode 100644 index 0000000..5819f3c --- /dev/null +++ b/view/Form_submit_page.py @@ -0,0 +1,16 @@ +from Tags import Html, Head, Body, Script + + +class Form_submit_page( Html ): + def __init__( self, form ): + Html.__init__( + self, + Head(), + Body( + form, + Script( # auto-submit the form + u"document.forms[ 0 ].submit();", + type = u"text/javascript", + ), + ), + ) diff --git a/view/Front_page.py b/view/Front_page.py index 6fc2c81..44b59bb 100644 --- a/view/Front_page.py +++ b/view/Front_page.py @@ -132,7 +132,7 @@ class Front_page( Product_page ): separator = u"", ), Div( - u"-Scott Tiner", + u"-Scott Tiner, Technical Writer", class_ = u"quote_signature" ), class_ = u"quote", diff --git a/view/Main_page.py b/view/Main_page.py index 351c5bf..bdd85d8 100644 --- a/view/Main_page.py +++ b/view/Main_page.py @@ -32,6 +32,7 @@ class Main_page( Page ): invites = None, invite_id = None, after_login = None, + signup_plan = None, ): startup_note_ids = [ startup_note.object_id for startup_note in startup_notes ] @@ -108,6 +109,7 @@ class Main_page( Page ): Input( type = u"hidden", name = u"invites", id = u"invites", value = json( invites ) ), Input( type = u"hidden", name = u"invite_id", id = u"invite_id", value = invite_id ), Input( type = u"hidden", name = u"after_login", id = u"after_login", value = after_login ), + Input( type = u"hidden", name = u"signup_plan", id = u"signup_plan", value = signup_plan ), Div( id = u"status_area", ), diff --git a/view/Upgrade_page.py b/view/Upgrade_page.py index 4cc2446..c29a178 100644 --- a/view/Upgrade_page.py +++ b/view/Upgrade_page.py @@ -98,8 +98,8 @@ class Upgrade_page( Product_page ): alt = u"More room to stretch out", ), Ul( - Li( u"More room for your wiki notes." ), - Li( u"More room for your documents and files." ), + Li( u"More space for your wiki notes." ), + Li( u"More space for your documents and files." ), class_ = u"upgrade_text", ), Img( @@ -176,7 +176,8 @@ class Upgrade_page( Product_page ): class_ = u"price_text", separator = u"", ), - user and user.username and user.rate_plan != index and plan.get( u"button" ).strip() and plan.get( u"button" ) % user.object_id, + user and user.username not in ( u"anonymous", None ) and user.rate_plan != index \ + and plan.get( u"button" ).strip() and plan.get( u"button" ) % user.object_id or None, ) or None, ( not user or user.username in ( u"anonymous", None ) ) and Div( A(