Browse Source

Finished remake of signup page. You can now click "signup" for non-free accounts even if you're not logged in.

Dan Helfman 10 years ago
parent
commit
c452408106

+ 8
- 1
controller/Root.py View File

@@ -56,9 +56,10 @@ class Root( object ):
56 56
     note_title = unicode,
57 57
     invite_id = Valid_id( none_okay = True ),
58 58
     after_login = Valid_string( min = 0, max = 100 ),
59
+    plan = Valid_int( none_okay = True ),
59 60
     user_id = Valid_id( none_okay = True ),
60 61
   )
61
-  def default( self, note_title, invite_id = None, after_login = None, user_id = None ):
62
+  def default( self, note_title, invite_id = None, after_login = None, plan = None, user_id = None ):
62 63
     """
63 64
     Convenience method for accessing a note in the main notebook by name rather than by note id.
64 65
 
@@ -68,6 +69,8 @@ class Root( object ):
68 69
     @param invite_id: id of the invite used to get to this note (optional)
69 70
     @type after_login: unicode
70 71
     @param after_login: URL to redirect to after login (optional, must start with "/")
72
+    @type plan: int
73
+    @param plan: rate plan index (optional, defaults to None)
71 74
     @rtype: unicode
72 75
     @return: rendered HTML page
73 76
     """
@@ -81,6 +84,8 @@ class Root( object ):
81 84
         return dict( redirect = u"%s/%s?invite_id=%s" % ( https_url, note_title, invite_id ) )
82 85
       if after_login:
83 86
         return dict( redirect = u"%s/%s?after_login=%s" % ( https_url, note_title, after_login ) )
87
+      if plan:
88
+        return dict( redirect = u"%s/%s?plan=%s" % ( https_url, note_title, plan ) )
84 89
       else:
85 90
         return dict( redirect = u"%s/%s" % ( https_url, note_title ) )
86 91
 
@@ -100,6 +105,8 @@ class Root( object ):
100 105
       result[ "invite_id" ] = invite_id
101 106
     if after_login and after_login.startswith( u"/" ):
102 107
       result[ "after_login" ] = after_login
108
+    if plan:
109
+      result[ "signup_plan" ] = plan
103 110
 
104 111
     return result
105 112
 

+ 43
- 2
controller/Users.py View File

@@ -10,7 +10,7 @@ from model.Note import Note
10 10
 from model.Password_reset import Password_reset
11 11
 from model.Invite import Invite
12 12
 from Expose import expose
13
-from Validate import validate, Valid_string, Valid_bool, Validation_error
13
+from Validate import validate, Valid_string, Valid_bool, Valid_int, Validation_error
14 14
 from Database import Valid_id, end_transaction
15 15
 from Expire import strongly_expire
16 16
 from view.Json import Json
@@ -21,6 +21,7 @@ from view.Blank_page import Blank_page
21 21
 from view.Thanks_note import Thanks_note
22 22
 from view.Thanks_error_note import Thanks_error_note
23 23
 from view.Processing_note import Processing_note
24
+from view.Form_submit_page import Form_submit_page
24 25
 
25 26
 
26 27
 USERNAME_PATTERN = re.compile( "^[a-zA-Z0-9]+$" )
@@ -205,8 +206,9 @@ class Users( object ):
205 206
     email_address = ( Valid_string( min = 0, max = 60 ) ),
206 207
     signup_button = unicode,
207 208
     invite_id = Valid_id( none_okay = True ),
209
+    rate_plan = Valid_int( none_okay = True ),
208 210
   )
209
-  def signup( self, username, password, password_repeat, email_address, signup_button, invite_id = None ):
211
+  def signup( self, username, password, password_repeat, email_address, signup_button, invite_id = None, rate_plan = None ):
210 212
     """
211 213
     Create a new User based on the given information. Start that user with their own Notebook and a
212 214
     "welcome to your wiki" Note. For convenience, login the newly created user as well.
@@ -223,6 +225,9 @@ class Users( object ):
223 225
     @param signup_button: ignored
224 226
     @type invite_id: unicode
225 227
     @param invite_id: id of invite to redeem upon signup (optional)
228
+    @type rate_plan: int
229
+    @param rate_plan: index of rate plan to signup for (optional). if greater than zero, redirect
230
+                      to PayPal subscribe page after signup
226 231
     @rtype: json dict
227 232
     @return: { 'redirect': url, 'authenticated': userdict }
228 233
     @raise Signup_error: passwords don't match or the username is unavailable
@@ -275,6 +280,9 @@ class Users( object ):
275 280
 
276 281
       self.convert_invite_to_access( invite, user_id )
277 282
       redirect = u"/notebooks/%s" % invite.notebook_id
283
+    # if there's a requested rate plan, then redirect to the PayPal subscribe page
284
+    elif rate_plan and rate_plan > 0:
285
+      redirect = u"/users/subscribe?rate_plan=%s" % rate_plan
278 286
     # otherwise, just redirect to the newly created notebook
279 287
     else:
280 288
       redirect = u"/notebooks/%s" % notebook.object_id
@@ -284,6 +292,39 @@ class Users( object ):
284 292
       authenticated = user,
285 293
     )
286 294
 
295
+  @expose( view = Form_submit_page )
296
+  @grab_user_id
297
+  @validate(
298
+    rate_plan = Valid_int(),
299
+    user_id = Valid_id(),
300
+  )
301
+  def subscribe( self, rate_plan, user_id ):
302
+    """
303
+    Submit a subscription form to PayPal, allowing the user to subscribe to the given rate plan.
304
+
305
+    @type rate_plan: int
306
+    @param rate_plan: index of rate plan to subscribe to
307
+    @type user_id: unicode
308
+    @param user_id: id of current logged-in user
309
+    @rtype: dict
310
+    @return: { 'form': subscription_form_html }
311
+    @raise Signup_error: invalid rate plan, no logged-in user, or missing subscribe button
312
+    """
313
+    if rate_plan == 0 or rate_plan >= len( self.__rate_plans ):
314
+      raise Signup_error( u"The rate plan is invalid." )
315
+
316
+    plan = self.__rate_plans[ rate_plan ]
317
+    button = plan.get( u"button" )
318
+    if not button or not button.strip():
319
+      raise Signup_error(
320
+        u"Sorry, that rate plan is not configured for subscriptions. Please contact %s." % \
321
+        ( self.__support_email or u"support" )
322
+      )
323
+
324
+    return dict(
325
+      form = button % user_id,
326
+    )
327
+
287 328
   @expose()
288 329
   @end_transaction
289 330
   @grab_user_id

+ 3
- 1
controller/Validate.py View File

@@ -142,12 +142,14 @@ class Valid_int( object ):
142 142
   """
143 143
   Validator for an integer value.
144 144
   """
145
-  def __init__( self, min = None, max = None ):
145
+  def __init__( self, min = None, max = None, none_okay = False ):
146 146
     self.min = min
147 147
     self.max = max
148 148
     self.message = None
149
+    self.__none_okay = none_okay
149 150
 
150 151
   def __call__( self, value ):
152
+    if self.__none_okay and value in ( None, "None", "" ): return None
151 153
     value = int( value )
152 154
 
153 155
     if self.min is not None and value < self.min:

+ 15
- 0
controller/test/Test_root.py View File

@@ -238,6 +238,21 @@ class Test_root( Test_controller ):
238 238
     assert result.get( u"after_login" ) is None
239 239
     assert result[ u"user" ].object_id == self.anonymous.object_id
240 240
 
241
+  def test_default_with_plan( self ):
242
+    plan = u"17"
243
+
244
+    result = self.http_get(
245
+      "/my_note?plan=%s" % plan,
246
+    )
247
+
248
+    assert result
249
+    assert result[ u"notes" ]
250
+    assert len( result[ u"notes" ] ) == 1
251
+    assert result[ u"notes" ][ 0 ].object_id == self.anon_note.object_id
252
+    assert result[ u"notebook" ].object_id == self.anon_notebook.object_id
253
+    assert result[ u"signup_plan" ] == 17
254
+    assert result[ u"user" ].object_id == self.anonymous.object_id
255
+
241 256
   def test_default_after_login( self ):
242 257
     self.login()
243 258
 

+ 120
- 0
controller/test/Test_users.py View File

@@ -93,6 +93,18 @@ class Test_users( Test_controller ):
93 93
 
94 94
     assert result[ u"redirect" ].startswith( u"/notebooks/" )
95 95
 
96
+  def test_signup_with_rate_plan( self ):
97
+    result = self.http_post( "/users/signup", dict(
98
+      username = self.new_username,
99
+      password = self.new_password,
100
+      password_repeat = self.new_password,
101
+      email_address = self.new_email_address,
102
+      signup_button = u"sign up",
103
+      rate_plan = u"2",
104
+    ) )
105
+
106
+    assert result[ u"redirect" ] == u"/users/subscribe?rate_plan=2"
107
+
96 108
   def test_signup_without_email_address( self ):
97 109
     result = self.http_post( "/users/signup", dict(
98 110
       username = self.new_username,
@@ -250,6 +262,62 @@ class Test_users( Test_controller ):
250 262
     assert rate_plan[ u"name" ] == u"super"
251 263
     assert rate_plan[ u"storage_quota_bytes" ] == 1337 * 10
252 264
 
265
+  def test_current_after_signup_with_rate_plan( self ):
266
+    result = self.http_post( "/users/signup", dict(
267
+      username = self.new_username,
268
+      password = self.new_password,
269
+      password_repeat = self.new_password,
270
+      email_address = self.new_email_address,
271
+      signup_button = u"sign up",
272
+      rate_plan = u"2",
273
+    ) )
274
+    session_id = result[ u"session_id" ]
275
+
276
+    assert result[ u"redirect" ] == u"/users/subscribe?rate_plan=2"
277
+
278
+    user = self.database.last_saved_obj
279
+    assert isinstance( user, User )
280
+    result = cherrypy.root.users.current( user.object_id )
281
+
282
+    assert result[ u"user" ].object_id == user.object_id
283
+    assert result[ u"user" ].username == self.new_username
284
+    assert result[ u"user" ].email_address == self.new_email_address
285
+
286
+    notebooks = result[ u"notebooks" ]
287
+    notebook = notebooks[ 0 ]
288
+    assert notebook.object_id
289
+    assert notebook.revision
290
+    assert notebook.name == u"my notebook"
291
+    assert notebook.trash_id
292
+    assert notebook.read_write == True
293
+    assert notebook.owner == True
294
+    assert notebook.rank == 0
295
+
296
+    notebook = notebooks[ 1 ]
297
+    assert notebook.object_id == notebooks[ 0 ].trash_id
298
+    assert notebook.revision
299
+    assert notebook.name == u"trash"
300
+    assert notebook.trash_id == None
301
+    assert notebook.read_write == True
302
+    assert notebook.owner == True
303
+    assert notebook.rank == None
304
+
305
+    notebook = notebooks[ 2 ]
306
+    assert notebook.object_id == self.anon_notebook.object_id
307
+    assert notebook.revision == self.anon_notebook.revision
308
+    assert notebook.name == self.anon_notebook.name
309
+    assert notebook.trash_id == None
310
+    assert notebook.read_write == False
311
+    assert notebook.owner == False
312
+    assert notebook.rank == None
313
+
314
+    assert result.get( u"login_url" ) is None
315
+    assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/users/logout"
316
+
317
+    rate_plan = result[ u"rate_plan" ]
318
+    assert rate_plan[ u"name" ] == u"super"
319
+    assert rate_plan[ u"storage_quota_bytes" ] == 1337 * 10
320
+
253 321
   def test_signup_with_different_passwords( self ):
254 322
     result = self.http_post( "/users/signup", dict(
255 323
       username = self.new_username,
@@ -261,6 +329,58 @@ class Test_users( Test_controller ):
261 329
 
262 330
     assert result[ u"error" ]
263 331
 
332
+  def test_subscribe( self ):
333
+    self.login()
334
+
335
+    result = self.http_post( "/users/subscribe", dict(
336
+      rate_plan = u"1",
337
+    ), session_id = self.session_id )
338
+
339
+    form = result.get( u"form" )
340
+    plan = self.settings[ u"global" ][ u"luminotes.rate_plans" ][ 1 ]
341
+
342
+    assert form == plan[ u"button" ] % self.user.object_id
343
+
344
+  def test_subscribe_with_free_rate_plan( self ):
345
+    self.login()
346
+
347
+    result = self.http_post( "/users/subscribe", dict(
348
+      rate_plan = u"0",
349
+    ), session_id = self.session_id )
350
+
351
+    assert u"plan" in result[ u"error" ]
352
+    assert u"invalid" in result[ u"error" ]
353
+
354
+  def test_subscribe_with_invalid_rate_plan( self ):
355
+    self.login()
356
+
357
+    result = self.http_post( "/users/subscribe", dict(
358
+      rate_plan = u"17",
359
+    ), session_id = self.session_id )
360
+
361
+    assert u"plan" in result[ u"error" ]
362
+    assert u"invalid" in result[ u"error" ]
363
+
364
+  def test_subscribe_without_login( self ):
365
+    result = self.http_post( "/users/subscribe", dict(
366
+      rate_plan = u"1",
367
+    ) )
368
+
369
+    assert u"user" in result[ u"error" ]
370
+    assert u"invalid" in result[ u"error" ]
371
+
372
+  def test_subscribe_without_subscribe_button( self ):
373
+    self.login()
374
+    self.settings[ u"global" ][ u"luminotes.rate_plans" ][ 1 ][ u"button" ] = u"  "
375
+
376
+    result = self.http_post( "/users/subscribe", dict(
377
+      rate_plan = u"1",
378
+    ), session_id = self.session_id )
379
+
380
+
381
+    print result
382
+    assert u"not configured" in result[ u"error" ]
383
+
264 384
   def test_demo( self ):
265 385
     result = self.http_post( "/users/demo", dict() )
266 386
 

+ 1
- 0
static/css/product.css View File

@@ -348,6 +348,7 @@
348 348
 
349 349
 .upgrade_left_area {
350 350
   width: 400px;
351
+  margin-top: 1.5em;
351 352
   margin-bottom: 1em;
352 353
 }
353 354
 

BIN
static/images/sign_up_button.png View File


BIN
static/images/sign_up_button.xcf View File


BIN
static/images/subscribe_button.png View File


BIN
static/images/subscribe_button.xcf View File


BIN
static/images/unsubscribe_button.png View File


BIN
static/images/unsubscribe_button.xcf View File


+ 6
- 3
static/js/Wiki.js View File

@@ -17,6 +17,7 @@ function Wiki( invoker ) {
17 17
   this.invites = evalJSON( getElement( "invites" ).value );
18 18
   this.invite_id = getElement( "invite_id" ).value;
19 19
   this.after_login = getElement( "after_login" ).value;
20
+  this.signup_plan = getElement( "signup_plan" ).value;
20 21
   this.font_size = null;
21 22
 
22 23
   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
693 694
   connect( editor, "invites_updated", function ( invites ) { self.invites = invites; self.share_notebook(); } );
694 695
   connect( editor, "submit_form", function ( url, form, callback ) {
695 696
     var args = {}
696
-    if ( url == "/users/signup" || url == "/users/login" ) {
697
+    if ( url == "/users/signup" ) {
697 698
       args[ "invite_id" ] = self.invite_id;
698
-      if ( url == "/users/login" )
699
-        args[ "after_login" ] = self.after_login;
699
+      args[ "rate_plan" ] = self.signup_plan;
700
+    } else if ( url == "/users/login" ) {
701
+      args[ "invite_id" ] = self.invite_id;
702
+      args[ "after_login" ] = self.after_login;
700 703
     }
701 704
 
702 705
     self.invoker.invoke( url, "POST", args, callback, form );

+ 16
- 0
view/Form_submit_page.py View File

@@ -0,0 +1,16 @@
1
+from Tags import Html, Head, Body, Script
2
+
3
+
4
+class Form_submit_page( Html ):
5
+  def __init__( self, form ):
6
+    Html.__init__(
7
+      self,
8
+      Head(),
9
+      Body(
10
+        form,
11
+        Script( # auto-submit the form
12
+          u"document.forms[ 0 ].submit();",
13
+          type = u"text/javascript",
14
+        ),
15
+      ),
16
+    )

+ 1
- 1
view/Front_page.py View File

@@ -132,7 +132,7 @@ class Front_page( Product_page ):
132 132
               separator = u"",
133 133
             ),
134 134
             Div(
135
-              u"-Scott Tiner",
135
+              u"-Scott Tiner, Technical Writer",
136 136
               class_ = u"quote_signature"
137 137
             ),
138 138
             class_ = u"quote",

+ 2
- 0
view/Main_page.py View File

@@ -32,6 +32,7 @@ class Main_page( Page ):
32 32
     invites = None,
33 33
     invite_id = None,
34 34
     after_login = None,
35
+    signup_plan = None,
35 36
   ):
36 37
     startup_note_ids = [ startup_note.object_id for startup_note in startup_notes ]
37 38
 
@@ -108,6 +109,7 @@ class Main_page( Page ):
108 109
       Input( type = u"hidden", name = u"invites", id = u"invites", value = json( invites ) ),
109 110
       Input( type = u"hidden", name = u"invite_id", id = u"invite_id", value = invite_id ),
110 111
       Input( type = u"hidden", name = u"after_login", id = u"after_login", value = after_login ),
112
+      Input( type = u"hidden", name = u"signup_plan", id = u"signup_plan", value = signup_plan ),
111 113
       Div(
112 114
         id = u"status_area",
113 115
       ),

+ 4
- 3
view/Upgrade_page.py View File

@@ -98,8 +98,8 @@ class Upgrade_page( Product_page ):
98 98
               alt = u"More room to stretch out",
99 99
             ),
100 100
             Ul(
101
-              Li( u"More room for your wiki notes." ),
102
-              Li( u"More room for your documents and files." ),
101
+              Li( u"More space for your wiki notes." ),
102
+              Li( u"More space for your documents and files." ),
103 103
               class_ = u"upgrade_text",
104 104
             ),
105 105
             Img(
@@ -176,7 +176,8 @@ class Upgrade_page( Product_page ):
176 176
             class_ = u"price_text",
177 177
             separator = u"",
178 178
           ),
179
-          user and user.username and user.rate_plan != index and plan.get( u"button" ).strip() and plan.get( u"button" ) % user.object_id,
179
+          user and user.username not in ( u"anonymous", None ) and user.rate_plan != index \
180
+               and plan.get( u"button" ).strip() and plan.get( u"button" ) % user.object_id or None,
180 181
         ) or None,
181 182
         ( not user or user.username in ( u"anonymous", None ) ) and Div(
182 183
           A(