Browse Source

Made upgrade page dynamically generated on the server instead of static html.

This allows things like only displaying subscription buttons if you're logged in.
Dan Helfman 10 years ago
parent
commit
8f00cceb94

+ 2
- 0
NEWS View File

@@ -3,6 +3,8 @@
3 3
  * Feature to preview a notebook as a viewer would see it.
4 4
  * Note revisions list now include username of the user who made that
5 5
    revision.
6
+ * If you go to luminotes.com when you're logged in, you'll be automatically
7
+   redirected to your first notebook.
6 8
  * Fixed bug where passwords with special characters broke password hashing.
7 9
  * Fixed bug that prevented you from opening a note with a title that looked
8 10
    like an external URL.

+ 15
- 5
config/Common.py View File

@@ -28,22 +28,32 @@ settings = {
28 28
         "name": "free",
29 29
         "storage_quota_bytes": 30 * MEGABYTE,
30 30
         "notebook_collaboration": False,
31
+        "fee": None,
31 32
       },
32 33
       {
33 34
         "name": "basic",
34 35
         "storage_quota_bytes": 250 * MEGABYTE,
35 36
         "notebook_collaboration": True,
37
+        "fee": 5,
38
+        "button":
39
+          """
40
+          """,
36 41
       },
37 42
       {
38 43
         "name": "standard",
39 44
         "storage_quota_bytes": 500 * MEGABYTE,
40 45
         "notebook_collaboration": True,
46
+        "fee": 9,
47
+        "button":
48
+          """
49
+          """,
41 50
       },
42
-      {
43
-        "name": "premium",
44
-        "storage_quota_bytes": 2000 * MEGABYTE,
45
-        "notebook_collaboration": True,
46
-      },
51
+#      {
52
+#        "name": "premium",
53
+#        "storage_quota_bytes": 2000 * MEGABYTE,
54
+#        "notebook_collaboration": True,
55
+#        "fee": 19,
56
+#      },
47 57
     ],
48 58
   },
49 59
 }

+ 43
- 3
controller/Root.py View File

@@ -2,7 +2,7 @@ import cherrypy
2 2
 
3 3
 from Expose import expose
4 4
 from Expire import strongly_expire
5
-from Validate import validate, Valid_int
5
+from Validate import validate, Valid_int, Valid_string
6 6
 from Notebooks import Notebooks
7 7
 from Users import Users, grab_user_id
8 8
 from Database import Valid_id
@@ -11,6 +11,7 @@ from model.Notebook import Notebook
11 11
 from model.User import User
12 12
 from view.Main_page import Main_page
13 13
 from view.Notebook_rss import Notebook_rss
14
+from view.Upgrade_note import Upgrade_note
14 15
 from view.Json import Json
15 16
 from view.Error_page import Error_page
16 17
 from view.Not_found_page import Not_found_page
@@ -47,9 +48,10 @@ class Root( object ):
47 48
   @validate(
48 49
     note_title = unicode,
49 50
     invite_id = Valid_id( none_okay = True ),
51
+    after_login = Valid_string( min = 0, max = 100 ),
50 52
     user_id = Valid_id( none_okay = True ),
51 53
   )
52
-  def default( self, note_title, invite_id = None, user_id = None ):
54
+  def default( self, note_title, invite_id = None, after_login = None, user_id = None ):
53 55
     """
54 56
     Convenience method for accessing a note in the main notebook by name rather than by note id.
55 57
 
@@ -57,6 +59,8 @@ class Root( object ):
57 59
     @param note_title: title of the note to return
58 60
     @type invite_id: unicode
59 61
     @param invite_id: id of the invite used to get to this note (optional)
62
+    @type after_login: unicode
63
+    @param after_login: URL to redirect to after login (optional, must start with "/")
60 64
     @rtype: unicode
61 65
     @return: rendered HTML page
62 66
     """
@@ -85,6 +89,8 @@ class Root( object ):
85 89
     result.update( self.__notebooks.contents( main_notebook.object_id, user_id = user_id, note_id = note.object_id ) )
86 90
     if invite_id:
87 91
       result[ "invite_id" ] = invite_id
92
+    if after_login and after_login.startswith( u"/" ):
93
+      result[ "after_login" ] = after_login
88 94
 
89 95
     return result
90 96
 
@@ -144,7 +150,7 @@ class Root( object ):
144 150
         if user:
145 151
           first_notebook = self.__database.select_one( Notebook, user.sql_load_notebooks( parents_only = True, undeleted_only = True ) )
146 152
           if first_notebook:
147
-            return dict( redirect = "%s/notebooks/%s" % ( https_url, first_notebook.object_id ) )
153
+            return dict( redirect = u"%s/notebooks/%s" % ( https_url, first_notebook.object_id ) )
148 154
       
149 155
       # if the user is logged in and not using https, then redirect to the https version of the page (if available)
150 156
       if https_url and cherrypy.request.remote_addr != https_proxy_ip:
@@ -231,6 +237,40 @@ class Root( object ):
231 237
 
232 238
     return result
233 239
 
240
+  @expose( view = Main_page )
241
+  @grab_user_id
242
+  @validate(
243
+    user_id = Valid_id( none_okay = True ),
244
+  )
245
+  def upgrade( self, user_id = None ):
246
+    """
247
+    Provide the information necessary to display the Luminotes upgrade page.
248
+    """
249
+    anonymous = self.__database.select_one( User, User.sql_load_by_username( u"anonymous" ) )
250
+    if anonymous:
251
+      main_notebook = self.__database.select_one( Notebook, anonymous.sql_load_notebooks( undeleted_only = True ) )
252
+    else:
253
+      main_notebook = None
254
+
255
+    https_url = self.__settings[ u"global" ].get( u"luminotes.https_url" )
256
+    result = self.__users.current( user_id )
257
+    result[ "notebook" ] = main_notebook
258
+    result[ "startup_notes" ] = self.__database.select_many( Note, main_notebook.sql_load_startup_notes() )
259
+    result[ "total_notes_count" ] = self.__database.select_one( Note, main_notebook.sql_count_notes() )
260
+    result[ "note_read_write" ] = False
261
+    result[ "notes" ] = [ Note.create(
262
+      object_id = u"upgrade",
263
+      contents = unicode( Upgrade_note(
264
+        self.__settings[ u"global" ].get( u"luminotes.rate_plans", [] ),
265
+        https_url,
266
+        user_id,
267
+      ) ),
268
+      notebook_id = main_notebook.object_id,
269
+    ) ]
270
+    result[ "invites" ] = []
271
+
272
+    return result
273
+
234 274
   # TODO: move this method to controller.Notebooks, and maybe give it a more sensible name
235 275
   @expose( view = Json )
236 276
   def next_id( self ):

+ 7
- 1
controller/Users.py View File

@@ -331,8 +331,9 @@ class Users( object ):
331 331
     password = Valid_string( min = 1, max = 30 ),
332 332
     login_button = unicode,
333 333
     invite_id = Valid_id( none_okay = True ),
334
+    after_login = Valid_string( min = 0, max = 100 ),
334 335
   )
335
-  def login( self, username, password, login_button, invite_id = None ):
336
+  def login( self, username, password, login_button, invite_id = None, after_login = None ):
336 337
     """
337 338
     Attempt to authenticate the user. If successful, associate the given user with the current
338 339
     session.
@@ -343,6 +344,8 @@ class Users( object ):
343 344
     @param password: the user's password
344 345
     @type invite_id: unicode
345 346
     @param invite_id: id of invite to redeem upon login (optional)
347
+    @type after_login: unicode
348
+    @param after_login: URL to redirect to after login (optional, must start with "/")
346 349
     @rtype: json dict
347 350
     @return: { 'redirect': url, 'authenticated': userdict }
348 351
     @raise Authentication_error: invalid username or password
@@ -363,6 +366,9 @@ class Users( object ):
363 366
 
364 367
       self.convert_invite_to_access( invite, user.object_id )
365 368
       redirect = u"/notebooks/%s" % invite.notebook_id
369
+    # if there's an after_login URL, redirect to it
370
+    elif after_login and after_login.startswith( "/" ):
371
+      redirect = after_login
366 372
     # otherwise, just redirect to the user's first notebook (if any)
367 373
     elif first_notebook:
368 374
       redirect = u"/notebooks/%s" % first_notebook.object_id

+ 6
- 0
controller/test/Test_controller.py View File

@@ -314,10 +314,16 @@ class Test_controller( object ):
314 314
           {
315 315
             u"name": u"super",
316 316
             u"storage_quota_bytes": 1337,
317
+            u"notebook_collaboration": True,
318
+            u"fee": 1.99,
319
+            u"button": u"[subscribe here user %s!] button",
317 320
           },
318 321
           {
319 322
             u"name": "extra super",
320 323
             u"storage_quota_bytes": 31337,
324
+            u"notebook_collaboration": True,
325
+            u"fee": 199.99,
326
+            u"button": u"[or here user %s!] button",
321 327
           },
322 328
         ],
323 329
       },

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

@@ -117,6 +117,36 @@ class Test_root( Test_controller ):
117 117
     assert result[ u"invite_id" ] == u"whee"
118 118
     assert result[ u"user" ].object_id == self.anonymous.object_id
119 119
 
120
+  def test_default_with_after_login( self ):
121
+    after_login = "/foo/bar"
122
+
123
+    result = self.http_get(
124
+      "/my_note?after_login=%s" % after_login,
125
+    )
126
+
127
+    assert result
128
+    assert result[ u"notes" ]
129
+    assert len( result[ u"notes" ] ) == 1
130
+    assert result[ u"notes" ][ 0 ].object_id == self.anon_note.object_id
131
+    assert result[ u"notebook" ].object_id == self.anon_notebook.object_id
132
+    assert result[ u"after_login" ] == after_login
133
+    assert result[ u"user" ].object_id == self.anonymous.object_id
134
+
135
+  def test_default_with_after_login_with_full_url( self ):
136
+    after_login = "http://example.com/foo/bar"
137
+
138
+    result = self.http_get(
139
+      "/my_note?after_login=%s" % after_login,
140
+    )
141
+
142
+    assert result
143
+    assert result[ u"notes" ]
144
+    assert len( result[ u"notes" ] ) == 1
145
+    assert result[ u"notes" ][ 0 ].object_id == self.anon_note.object_id
146
+    assert result[ u"notebook" ].object_id == self.anon_notebook.object_id
147
+    assert result.get( u"after_login" ) is None
148
+    assert result[ u"user" ].object_id == self.anonymous.object_id
149
+
120 150
   def test_default_after_login( self ):
121 151
     self.login()
122 152
 
@@ -205,6 +235,72 @@ class Test_root( Test_controller ):
205 235
     assert u"error" not in result
206 236
     assert result[ u"notebook" ].object_id == self.privacy_notebook.object_id
207 237
 
238
+  def test_upgrade( self ):
239
+    result = self.http_get( "/upgrade" )
240
+
241
+    assert result[ u"user" ].username == u"anonymous"
242
+    assert len( result[ u"notebooks" ] ) == 4
243
+    assert result[ u"notebooks" ][ 0 ].object_id == self.anon_notebook.object_id
244
+    assert result[ u"notebooks" ][ 0 ].name == self.anon_notebook.name
245
+    assert result[ u"notebooks" ][ 0 ].read_write == False
246
+    assert result[ u"notebooks" ][ 0 ].owner == False
247
+
248
+    rate_plan = result[ u"rate_plan" ]
249
+    assert rate_plan
250
+    assert rate_plan[ u"name" ] == u"super"
251
+    assert rate_plan[ u"storage_quota_bytes" ] == 1337
252
+
253
+    assert result[ u"notebook" ].object_id == self.anon_notebook.object_id
254
+    assert len( result[ u"startup_notes" ] ) == 0
255
+    assert result[ u"note_read_write" ] is False
256
+
257
+    assert result[ u"notes" ]
258
+    assert len( result[ u"notes" ] ) == 1
259
+    assert result[ u"notes" ][ 0 ].title == u"upgrade your wiki"
260
+    assert result[ u"notes" ][ 0 ].notebook_id == self.anon_notebook.object_id
261
+
262
+    contents = result[ u"notes" ][ 0 ].contents
263
+    assert u"upgrade" in contents
264
+    assert u"Super" in contents
265
+    assert u"Extra super" in contents
266
+
267
+    # since the user is not logged in, no subscription buttons should be shown
268
+    assert u"button" not in contents
269
+
270
+  def test_upgrade_after_login( self ):
271
+    self.login()
272
+
273
+    result = self.http_get( "/upgrade", session_id = self.session_id )
274
+
275
+    assert result[ u"user" ].username == self.username
276
+    assert len( result[ u"notebooks" ] ) == 5
277
+    assert result[ u"notebooks" ][ 0 ].object_id == self.notebook.object_id
278
+    assert result[ u"notebooks" ][ 0 ].name == self.notebook.name
279
+    assert result[ u"notebooks" ][ 0 ].read_write == False
280
+    assert result[ u"notebooks" ][ 0 ].owner == False
281
+
282
+    rate_plan = result[ u"rate_plan" ]
283
+    assert rate_plan
284
+    assert rate_plan[ u"name" ] == u"super"
285
+    assert rate_plan[ u"storage_quota_bytes" ] == 1337
286
+
287
+    assert result[ u"notebook" ].object_id == self.anon_notebook.object_id
288
+    assert len( result[ u"startup_notes" ] ) == 0
289
+    assert result[ u"note_read_write" ] is False
290
+
291
+    assert result[ u"notes" ]
292
+    assert len( result[ u"notes" ] ) == 1
293
+    assert result[ u"notes" ][ 0 ].title == u"upgrade your wiki"
294
+    assert result[ u"notes" ][ 0 ].notebook_id == self.anon_notebook.object_id
295
+
296
+    contents = result[ u"notes" ][ 0 ].contents
297
+    assert u"upgrade" in contents
298
+    assert u"Super" in contents
299
+    assert u"Extra super" in contents
300
+
301
+    # since the user is logged in, subscription buttons should be shown
302
+    assert u"button" in contents
303
+
208 304
   def test_next_id( self ):
209 305
     result = self.http_get( "/next_id" )
210 306
 

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

@@ -457,6 +457,30 @@ class Test_users( Test_controller ):
457 457
     assert cherrypy.root.users.check_access( self.user2.object_id, self.notebooks[ 0 ].object_id )
458 458
     assert cherrypy.root.users.check_access( self.user2.object_id, self.notebooks[ 0 ].trash_id )
459 459
 
460
+  def test_current_after_login_with_after_login( self ):
461
+    after_login = u"/foo/bar"
462
+
463
+    result = self.http_post( "/users/login", dict(
464
+      username = self.username2,
465
+      password = self.password2,
466
+      after_login = after_login,
467
+      login_button = u"login",
468
+    ) )
469
+
470
+    assert result[ u"redirect" ] == after_login
471
+
472
+  def test_current_after_login_with_after_login_with_full_url( self ):
473
+    after_login = u"http://this_url/does/not/start/with/a/slash"
474
+
475
+    result = self.http_post( "/users/login", dict(
476
+      username = self.username2,
477
+      password = self.password2,
478
+      after_login = after_login,
479
+      login_button = u"login",
480
+    ) )
481
+
482
+    assert result[ u"redirect" ] == u"/"
483
+
460 484
   def test_update_storage( self ):
461 485
     previous_revision = self.user.revision
462 486
 

+ 49
- 6
static/css/note.css View File

@@ -138,9 +138,30 @@ ol li {
138 138
   padding-right: 1em;
139 139
 }
140 140
 
141
+#upgrade_table_area {
142
+  text-align: center;
143
+}
144
+
145
+#upgrade_login_text {
146
+  font-weight: bold;
147
+  text-align: center;
148
+}
149
+
141 150
 #upgrade_table {
142 151
   border-collapse: collapse;
143 152
   border: 1px solid #999999;
153
+  margin-left: auto;
154
+  margin-right: auto;
155
+}
156
+
157
+#upgrade_table th {
158
+  padding: 0.5em;
159
+}
160
+
161
+#upgrade_table td {
162
+  text-align: center;
163
+  background-color: #fafafa;
164
+  padding: 0.5em;
144 165
 }
145 166
 
146 167
 #upgrade_table .plan_name {
@@ -155,21 +176,43 @@ ol li {
155 176
   background-color: #fafafa;
156 177
 }
157 178
 
158
-#upgrade_table .price_text {
159
-  color: #ff6600;
179
+#upgrade_table_small {
180
+  border-collapse: collapse;
181
+  border: 1px solid #999999;
182
+  margin-left: auto;
183
+  margin-right: auto;
160 184
 }
161 185
 
162
-#upgrade_table .month_text {
163
-  padding-top: 0.5em;
164
-  font-size: 75%;
186
+#upgrade_table_small th {
187
+  padding: 0.5em;
165 188
 }
166 189
 
167
-#upgrade_table td {
190
+#upgrade_table_small td {
168 191
   text-align: center;
169 192
   background-color: #fafafa;
170 193
   padding: 0.5em;
171 194
 }
172 195
 
196
+#upgrade_table_small .plan_name {
197
+  width: 33%;
198
+  text-align: center;
199
+  background-color: #d0e0f0;
200
+}
201
+
202
+.price_text {
203
+  color: #ff6600;
204
+}
205
+
206
+.month_text {
207
+  padding-top: 0.5em;
208
+  font-size: 75%;
209
+}
210
+
211
+.subscribe_form {
212
+  margin-top: 0.5em;
213
+  margin-bottom: 0;
214
+}
215
+
173 216
 .thumbnail_left {
174 217
   float: left;
175 218
   border-width: 0;

+ 5
- 4
static/html/faq.html View File

@@ -16,10 +16,11 @@ whenever you want.</p>
16 16
 
17 17
 <b>Does this cost me anything?</b><br />
18 18
 
19
-<p>Nope, use of your personal Luminotes wiki is completely free. Soon you will
20
-also be able to <a href="/notebooks/%s?note_id=new">upgrade</a> your Luminotes
21
-account to get notebook sharing features and additional storage space. But the
22
-features you're using now will always remain free.</p>
19
+<p>Use of your personal Luminotes wiki is completely free. You also have the
20
+option of <a href="/upgrade" target="_top">upgrading</a> your Luminotes
21
+account to get notebook sharing features and additional storage space for a
22
+reasonable subscription fee. But the features you're using now will always
23
+remain free.</p>
23 24
 
24 25
 <b>What does Luminotes run on?</b><br />
25 26
 

+ 2
- 1
static/html/navigation.html View File

@@ -8,7 +8,8 @@
8 8
 <a href="/notebooks/%s?note_id=new">faq</a> -
9 9
 <a href="/blog" target="_top">blog</a> -
10 10
 <a href="/guide" target="_top">user guide</a> -
11
+<a href="/upgrade" target="_top">pricing</a> -
11 12
 <a href="/notebooks/%s?note_id=new">meet the team</a> -
12 13
 <a href="/notebooks/%s?note_id=new">contact info</a> -
13
-<a href="/privacy" target="_top">privacy policy</a>
14
+<a href="/privacy" target="_top">privacy</a>
14 15
 </div>

+ 0
- 53
static/html/upgrade.html View File

@@ -1,53 +0,0 @@
1
-<h3>upgrade</h3>
2
-
3
-<p>
4
-In a few short weeks, you'll be able to upgrade your Luminotes account to get
5
-notebook sharing features and additional storage space. Here are some of the
6
-features you can look forward to.
7
-</p>
8
-
9
-<h3>share your notebook</h3>
10
-
11
-<p>
12
-<a href="/static/images/share.png" target="_new"><img
13
-src="/static/images/share_thumb.png" class="thumbnail_right" width="200" height="200" /></a>
14
-Most of the time, you want to keep your personal wiki all to yourself. But
15
-sometimes you simply need to share your work with friends and colleagues. When
16
-you upgrade your Luminotes account, you'll be able to invite specific people
17
-to collaborate on your wiki simply by entering their email addresses. You can
18
-even give them full editing capbilities, so several people can contribute to
19
-your wiki notebook.
20
-</p>
21
-
22
-<h3>access control</h3>
23
-
24
-<p>
25
-<a href="/static/images/access.png" target="_new"><img
26
-src="/static/images/access_thumb.png" class="thumbnail_left" width="200" height="200" /></a>
27
-With an upgraded Luminotes wiki, you'll decide exactly how much access to give
28
-people. Collaborators can make changes to your notebook, while viewers can
29
-only read your wiki. And owners have the same complete access to your notebook
30
-that you do. When you're done collaborating, a single click revokes a user's
31
-notebook access.
32
-</p>
33
-
34
-<p>
35
-Your wiki access control works on a per-notebook basis, so you can easily
36
-share one notebook with your friends while keeping your other notebooks
37
-completely private.
38
-</p>
39
-
40
-<h3>additional storage space</h3>
41
-
42
-<p>
43
-An upgraded Luminotes account gets you more than just notebook sharing
44
-features. You'll also be treated to way more room for your personal wiki. That
45
-means you'll have more space for your notes and ideas, and you won't have to
46
-worry about running out of room anytime soon.
47
-</p>
48
-
49
-<h3>stay tuned</h3>
50
-
51
-More information about upgrading your Luminotes account will be added as it
52
-becomes available. Please consider subscribing to the <a href="/blog"
53
-target="_top">Luminotes blog</a> for updates!

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

@@ -13,7 +13,8 @@ function Wiki( invoker ) {
13 13
   this.rate_plan = evalJSON( getElement( "rate_plan" ).value );
14 14
   this.storage_usage_high = false;
15 15
   this.invites = evalJSON( getElement( "invites" ).value );
16
-  this.invite_id = getElement( "invite_id" ).value ;
16
+  this.invite_id = getElement( "invite_id" ).value;
17
+  this.after_login = getElement( "after_login" ).value;
17 18
 
18 19
   var total_notes_count_node = getElement( "total_notes_count" );
19 20
   if ( total_notes_count_node )
@@ -652,8 +653,11 @@ Wiki.prototype.create_editor = function ( id, note_text, deleted_from_id, revisi
652 653
   connect( editor, "invites_updated", function ( invites ) { self.invites = invites; self.share_notebook(); } );
653 654
   connect( editor, "submit_form", function ( url, form, callback ) {
654 655
     var args = {}
655
-    if ( url == "/users/signup" || url == "/users/login" )
656
+    if ( url == "/users/signup" || url == "/users/login" ) {
656 657
       args[ "invite_id" ] = self.invite_id;
658
+      if ( url == "/users/login" )
659
+        args[ "after_login" ] = self.after_login;
660
+    }
657 661
 
658 662
     self.invoker.invoke( url, "POST", args, callback, form );
659 663
   } );

+ 0
- 1
tools/initdb.py View File

@@ -24,7 +24,6 @@ class Initializer( object ):
24 24
     ( u"password reset.html", False ),
25 25
     ( u"advanced browser features.html", False ),
26 26
     ( u"supported browsers.html", False ),
27
-    ( u"upgrade.html", False ),
28 27
   ]
29 28
 
30 29
   def __init__( self, database, nuke = False ):

+ 0
- 1
tools/updatedb.py View File

@@ -24,7 +24,6 @@ class Updater( object ):
24 24
     ( u"password reset.html", False ),
25 25
     ( u"advanced browser features.html", False ),
26 26
     ( u"supported browsers.html", False ),
27
-    ( u"upgrade.html", False ),
28 27
   ]
29 28
 
30 29
   def __init__( self, database, navigation_note_id = None ):

+ 2
- 0
view/Main_page.py View File

@@ -31,6 +31,7 @@ class Main_page( Page ):
31 31
     deleted_id = None,
32 32
     invites = None,
33 33
     invite_id = None,
34
+    after_login = None,
34 35
   ):
35 36
     startup_note_ids = [ startup_note.object_id for startup_note in startup_notes ]
36 37
 
@@ -102,6 +103,7 @@ class Main_page( Page ):
102 103
       Input( type = u"hidden", name = u"deleted_id", id = u"deleted_id", value = deleted_id ),
103 104
       Input( type = u"hidden", name = u"invites", id = u"invites", value = json( invites ) ),
104 105
       Input( type = u"hidden", name = u"invite_id", id = u"invite_id", value = invite_id ),
106
+      Input( type = u"hidden", name = u"after_login", id = u"after_login", value = after_login ),
105 107
       Div(
106 108
         id = u"status_area",
107 109
       ),

+ 1
- 0
view/Notebook_rss.py View File

@@ -25,6 +25,7 @@ class Notebook_rss( Rss_channel ):
25 25
     deleted_id = None,
26 26
     invites = None,
27 27
     invite_id = None,
28
+    after_login = None,
28 29
   ):
29 30
     if notebook.name == u"Luminotes":
30 31
       notebook_path = u"/"

+ 0
- 2
view/Redeem_invite_note.py View File

@@ -3,8 +3,6 @@ from Tags import Span, H3, P, A
3 3
 
4 4
 class Redeem_invite_note( Span ):
5 5
   def __init__( self, invite, notebook ):
6
-    title = None
7
-
8 6
     Span.__init__(
9 7
       self,
10 8
       H3( notebook.name ),

+ 0
- 2
view/Redeem_reset_note.py View File

@@ -3,8 +3,6 @@ from Tags import Span, H3, P, Form, P, Div, Strong, Br, Input
3 3
 
4 4
 class Redeem_reset_note( Span ):
5 5
   def __init__( self, password_reset_id, users ):
6
-    title = None
7
-
8 6
     Span.__init__(
9 7
       self,
10 8
       H3( u"complete your password reset" ),

+ 184
- 0
view/Upgrade_note.py View File

@@ -0,0 +1,184 @@
1
+from Tags import Div, Span, H3, P, A, Table, Tr, Th, Td, Br, Img
2
+
3
+
4
+class Upgrade_note( Span ):
5
+  def __init__( self, rate_plans, https_url, user_id ):
6
+    MEGABYTE = 1024 * 1024
7
+
8
+    Span.__init__(
9
+      self,
10
+      H3( u"upgrade your wiki" ),
11
+      P(
12
+        u"When you",
13
+        A( u"sign up", href = https_url + u"/sign_up", target = u"_top" ),
14
+        """
15
+        for a free Luminotes account, you get a full-featured
16
+        personal wiki available wherever you go. And if you upgrade your
17
+        Luminotes account, you'll also get powerful notebook sharing features
18
+        so that you and your friends can all collaborate on your wiki notebook.
19
+        """,
20
+      ),
21
+      P(
22
+        Table(
23
+          self.fee_row( rate_plans, user_id ),
24
+          Tr(
25
+            Td( u"included storage space", class_ = u"feature_name" ),
26
+            [ Td(
27
+              plan[ u"storage_quota_bytes" ] // MEGABYTE, " MB",
28
+            ) for plan in rate_plans ],
29
+          ),
30
+          Tr(
31
+            Td( u"unlimited wiki notebooks", class_ = u"feature_name" ),
32
+            [ Td(
33
+              Img( src = u"/static/images/check.png", width = u"20", height = u"17" ),
34
+            ) for plan in rate_plans ],
35
+          ),
36
+          Tr(
37
+            Td( u"friendly email support", class_ = u"feature_name" ),
38
+            [ Td(
39
+              Img( src = u"/static/images/check.png", width = u"20", height = u"17" ),
40
+            ) for plan in rate_plans ],
41
+          ),
42
+          Tr(
43
+            Td( u"multi-user collaboration", class_ = u"feature_name" ),
44
+            [ Td(
45
+              plan[ u"notebook_collaboration" ] and
46
+              Img( src = u"/static/images/check.png", width = u"20", height = u"17" ) or u"&nbsp",
47
+            ) for plan in rate_plans ],
48
+          ),
49
+          Tr(
50
+            Td( u"wiki access control", class_ = u"feature_name" ),
51
+            [ Td(
52
+              plan[ u"notebook_collaboration" ] and
53
+              Img( src = u"/static/images/check.png", width = u"20", height = u"17" ) or u"&nbsp",
54
+            ) for plan in rate_plans ],
55
+          ),
56
+          border = u"1",
57
+          id = u"upgrade_table",
58
+        ),
59
+        ( not user_id ) and P(
60
+          u"To upgrade your Luminotes account, please",
61
+          A( u"login", href = https_url + u"/login?after_login=/upgrade", target = u"_top" ),
62
+          u"first!",
63
+          id = u"upgrade_login_text",
64
+        ) or None,
65
+        id = u"upgrade_table_area",
66
+      ),
67
+
68
+      H3( u"share your notebook" ),
69
+      P(
70
+        A(
71
+          Img(
72
+            src = u"/static/images/share_thumb.png",
73
+            class_ = u"thumbnail_right",
74
+            width = u"200",
75
+            height = u"200",
76
+          ),
77
+          href = u"/static/images/share.png",
78
+          target = u"_new",
79
+        ),
80
+        u"""
81
+        Most of the time, you want to keep your personal wiki all to yourself. But
82
+        sometimes you simply need to share your work with friends and colleagues.
83
+        """,
84
+      ),
85
+      P(
86
+        u"""
87
+        With an upgraded Luminotes account, you'll be able to invite specific people
88
+        to collaborate on your wiki simply by entering their email addresses. You can
89
+        even give them full editing capbilities, so several people can contribute to
90
+        your wiki notebook. And you can invite as many people as you want to
91
+        collaborate on your wiki. They only need to sign up for a free Luminotes
92
+        account to particpate.
93
+        """
94
+      ),
95
+      H3( u"wiki access control" ),
96
+      P(
97
+        A(
98
+          Img(
99
+            src = u"/static/images/access_thumb.png",
100
+            class_ = u"thumbnail_left",
101
+            width = u"200",
102
+            height = u"200",
103
+          ),
104
+          href = u"/static/images/access.png",
105
+          target = u"_new",
106
+        ),
107
+        u"""
108
+        With an upgraded Luminotes wiki, you'll decide exactly how much access to give
109
+        people. Collaborators can make changes to your notebook, while viewers can
110
+        only read your wiki. And owners have the same complete access to your notebook
111
+        that you do. When you're done collaborating, a single click revokes a user's
112
+        notebook access.
113
+        """,
114
+      ),
115
+      P(
116
+        u"""
117
+        Your wiki access control works on a per-notebook basis, so you can easily
118
+        share one notebook with your friends while keeping your other notebooks
119
+        completely private.
120
+        """,
121
+      ),
122
+      H3( u"additional storage space" ),
123
+      P(
124
+        u"""
125
+        An upgraded Luminotes account gets you more than just notebook sharing
126
+        features. You'll also be treated to way more room for your personal wiki. That
127
+        means you'll have more space for your notes and ideas, and you won't have to
128
+        worry about running out of room anytime soon.
129
+        """,
130
+      ),
131
+      H3( u"no questions asked money-back guarantee" ),
132
+      P(
133
+        u"""
134
+        If you upgrade your Luminotes account and find that it's not meeting your
135
+        needs, then simply request a refund within 30 days and your money will be
136
+        returned in full without any questions.
137
+        """
138
+      ),
139
+      P(
140
+        u"""
141
+        And no matter how long you've been using an upgraded Luminotes account, you
142
+        can cancel online anytime. You won't have to send email or talk to anyone in a
143
+        call center. If you do cancel, you keep all of your wiki notebooks and simply
144
+        return to a free account.
145
+        """,
146
+      ),
147
+      P(
148
+        Table(
149
+          self.fee_row( rate_plans, user_id, include_blank = False ),
150
+          Tr(
151
+            [ Td(
152
+              plan[ u"storage_quota_bytes" ] // MEGABYTE, " MB",
153
+            ) for plan in rate_plans ],
154
+          ),
155
+          border = u"1",
156
+          id = u"upgrade_table_small",
157
+        ),
158
+        ( not user_id ) and P(
159
+          u"Please",
160
+          A( u"login", href = https_url + u"/login?after_login=/upgrade", target = u"_top" ),
161
+          u"to upgrade your wiki!",
162
+          id = u"upgrade_login_text",
163
+        ) or None,
164
+        id = u"upgrade_table_area",
165
+      ),
166
+    )
167
+
168
+  def fee_row( self, rate_plans, user_id, include_blank = True ):
169
+    return Tr(
170
+      include_blank and Th( u"&nbsp;" ) or None,
171
+      [ Th(
172
+        plan[ u"name" ].capitalize(),
173
+        plan[ u"fee" ] and Div(
174
+          Span(
175
+            u"$%s" % plan[ u"fee" ],
176
+            Span( u"/month", class_ = u"month_text" ),
177
+            class_ = u"price_text",
178
+            separator = u"",
179
+          ),
180
+          user_id and plan.get( u"button" ) % user_id or None,
181
+        ) or None,
182
+        class_ = u"plan_name",
183
+      ) for plan in rate_plans ],
184
+    )