From 8f00cceb94f9e9e0c7cd232ca1647c5d9c53faee Mon Sep 17 00:00:00 2001
From: Dan Helfman
Date: Thu, 10 Jan 2008 08:33:19 +0000
Subject: [PATCH] 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.
---
NEWS | 2 +
config/Common.py | 20 +++-
controller/Root.py | 46 +++++++-
controller/Users.py | 8 +-
controller/test/Test_controller.py | 6 +
controller/test/Test_root.py | 96 +++++++++++++++
controller/test/Test_users.py | 24 ++++
static/css/note.css | 55 ++++++++-
static/html/faq.html | 9 +-
static/html/navigation.html | 3 +-
static/html/upgrade.html | 53 ---------
static/js/Wiki.js | 8 +-
tools/initdb.py | 1 -
tools/updatedb.py | 1 -
view/Main_page.py | 2 +
view/Notebook_rss.py | 1 +
view/Redeem_invite_note.py | 2 -
view/Redeem_reset_note.py | 2 -
view/Upgrade_note.py | 184 +++++++++++++++++++++++++++++
19 files changed, 442 insertions(+), 81 deletions(-)
delete mode 100644 static/html/upgrade.html
create mode 100644 view/Upgrade_note.py
diff --git a/NEWS b/NEWS
index 3a6485d..c2fa3e8 100644
--- a/NEWS
+++ b/NEWS
@@ -3,6 +3,8 @@
* Feature to preview a notebook as a viewer would see it.
* Note revisions list now include username of the user who made that
revision.
+ * If you go to luminotes.com when you're logged in, you'll be automatically
+ redirected to your first notebook.
* Fixed bug where passwords with special characters broke password hashing.
* Fixed bug that prevented you from opening a note with a title that looked
like an external URL.
diff --git a/config/Common.py b/config/Common.py
index 7113906..324a761 100644
--- a/config/Common.py
+++ b/config/Common.py
@@ -28,22 +28,32 @@ settings = {
"name": "free",
"storage_quota_bytes": 30 * MEGABYTE,
"notebook_collaboration": False,
+ "fee": None,
},
{
"name": "basic",
"storage_quota_bytes": 250 * MEGABYTE,
"notebook_collaboration": True,
+ "fee": 5,
+ "button":
+ """
+ """,
},
{
"name": "standard",
"storage_quota_bytes": 500 * MEGABYTE,
"notebook_collaboration": True,
+ "fee": 9,
+ "button":
+ """
+ """,
},
- {
- "name": "premium",
- "storage_quota_bytes": 2000 * MEGABYTE,
- "notebook_collaboration": True,
- },
+# {
+# "name": "premium",
+# "storage_quota_bytes": 2000 * MEGABYTE,
+# "notebook_collaboration": True,
+# "fee": 19,
+# },
],
},
}
diff --git a/controller/Root.py b/controller/Root.py
index c0c4e30..ae691ae 100644
--- a/controller/Root.py
+++ b/controller/Root.py
@@ -2,7 +2,7 @@ import cherrypy
from Expose import expose
from Expire import strongly_expire
-from Validate import validate, Valid_int
+from Validate import validate, Valid_int, Valid_string
from Notebooks import Notebooks
from Users import Users, grab_user_id
from Database import Valid_id
@@ -11,6 +11,7 @@ from model.Notebook import Notebook
from model.User import User
from view.Main_page import Main_page
from view.Notebook_rss import Notebook_rss
+from view.Upgrade_note import Upgrade_note
from view.Json import Json
from view.Error_page import Error_page
from view.Not_found_page import Not_found_page
@@ -47,9 +48,10 @@ class Root( object ):
@validate(
note_title = unicode,
invite_id = Valid_id( none_okay = True ),
+ after_login = Valid_string( min = 0, max = 100 ),
user_id = Valid_id( none_okay = True ),
)
- def default( self, note_title, invite_id = None, user_id = None ):
+ def default( self, note_title, invite_id = None, after_login = None, user_id = None ):
"""
Convenience method for accessing a note in the main notebook by name rather than by note id.
@@ -57,6 +59,8 @@ class Root( object ):
@param note_title: title of the note to return
@type invite_id: unicode
@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 "/")
@rtype: unicode
@return: rendered HTML page
"""
@@ -85,6 +89,8 @@ class Root( object ):
result.update( self.__notebooks.contents( main_notebook.object_id, user_id = user_id, note_id = note.object_id ) )
if invite_id:
result[ "invite_id" ] = invite_id
+ if after_login and after_login.startswith( u"/" ):
+ result[ "after_login" ] = after_login
return result
@@ -144,7 +150,7 @@ class Root( object ):
if user:
first_notebook = self.__database.select_one( Notebook, user.sql_load_notebooks( parents_only = True, undeleted_only = True ) )
if first_notebook:
- return dict( redirect = "%s/notebooks/%s" % ( https_url, first_notebook.object_id ) )
+ return dict( redirect = u"%s/notebooks/%s" % ( https_url, first_notebook.object_id ) )
# if the user is logged in and not using https, then redirect to the https version of the page (if available)
if https_url and cherrypy.request.remote_addr != https_proxy_ip:
@@ -231,6 +237,40 @@ class Root( object ):
return result
+ @expose( view = Main_page )
+ @grab_user_id
+ @validate(
+ user_id = Valid_id( none_okay = True ),
+ )
+ def upgrade( self, user_id = None ):
+ """
+ Provide the information necessary to display the Luminotes upgrade page.
+ """
+ anonymous = self.__database.select_one( User, User.sql_load_by_username( u"anonymous" ) )
+ if anonymous:
+ main_notebook = self.__database.select_one( Notebook, anonymous.sql_load_notebooks( undeleted_only = True ) )
+ else:
+ main_notebook = None
+
+ https_url = self.__settings[ u"global" ].get( u"luminotes.https_url" )
+ result = self.__users.current( user_id )
+ result[ "notebook" ] = main_notebook
+ result[ "startup_notes" ] = self.__database.select_many( Note, main_notebook.sql_load_startup_notes() )
+ result[ "total_notes_count" ] = self.__database.select_one( Note, main_notebook.sql_count_notes() )
+ result[ "note_read_write" ] = False
+ result[ "notes" ] = [ Note.create(
+ object_id = u"upgrade",
+ contents = unicode( Upgrade_note(
+ self.__settings[ u"global" ].get( u"luminotes.rate_plans", [] ),
+ https_url,
+ user_id,
+ ) ),
+ notebook_id = main_notebook.object_id,
+ ) ]
+ result[ "invites" ] = []
+
+ return result
+
# TODO: move this method to controller.Notebooks, and maybe give it a more sensible name
@expose( view = Json )
def next_id( self ):
diff --git a/controller/Users.py b/controller/Users.py
index 6cfacdf..699ee0d 100644
--- a/controller/Users.py
+++ b/controller/Users.py
@@ -331,8 +331,9 @@ class Users( object ):
password = Valid_string( min = 1, max = 30 ),
login_button = unicode,
invite_id = Valid_id( none_okay = True ),
+ after_login = Valid_string( min = 0, max = 100 ),
)
- def login( self, username, password, login_button, invite_id = None ):
+ def login( self, username, password, login_button, invite_id = None, after_login = None ):
"""
Attempt to authenticate the user. If successful, associate the given user with the current
session.
@@ -343,6 +344,8 @@ class Users( object ):
@param password: the user's password
@type invite_id: unicode
@param invite_id: id of invite to redeem upon login (optional)
+ @type after_login: unicode
+ @param after_login: URL to redirect to after login (optional, must start with "/")
@rtype: json dict
@return: { 'redirect': url, 'authenticated': userdict }
@raise Authentication_error: invalid username or password
@@ -363,6 +366,9 @@ class Users( object ):
self.convert_invite_to_access( invite, user.object_id )
redirect = u"/notebooks/%s" % invite.notebook_id
+ # if there's an after_login URL, redirect to it
+ elif after_login and after_login.startswith( "/" ):
+ redirect = after_login
# otherwise, just redirect to the user's first notebook (if any)
elif first_notebook:
redirect = u"/notebooks/%s" % first_notebook.object_id
diff --git a/controller/test/Test_controller.py b/controller/test/Test_controller.py
index 45079cc..ac1701f 100644
--- a/controller/test/Test_controller.py
+++ b/controller/test/Test_controller.py
@@ -314,10 +314,16 @@ class Test_controller( object ):
{
u"name": u"super",
u"storage_quota_bytes": 1337,
+ u"notebook_collaboration": True,
+ u"fee": 1.99,
+ u"button": u"[subscribe here user %s!] button",
},
{
u"name": "extra super",
u"storage_quota_bytes": 31337,
+ u"notebook_collaboration": True,
+ u"fee": 199.99,
+ u"button": u"[or here user %s!] button",
},
],
},
diff --git a/controller/test/Test_root.py b/controller/test/Test_root.py
index e67a977..a077e74 100644
--- a/controller/test/Test_root.py
+++ b/controller/test/Test_root.py
@@ -117,6 +117,36 @@ class Test_root( Test_controller ):
assert result[ u"invite_id" ] == u"whee"
assert result[ u"user" ].object_id == self.anonymous.object_id
+ def test_default_with_after_login( self ):
+ after_login = "/foo/bar"
+
+ result = self.http_get(
+ "/my_note?after_login=%s" % after_login,
+ )
+
+ 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"after_login" ] == after_login
+ assert result[ u"user" ].object_id == self.anonymous.object_id
+
+ def test_default_with_after_login_with_full_url( self ):
+ after_login = "http://example.com/foo/bar"
+
+ result = self.http_get(
+ "/my_note?after_login=%s" % after_login,
+ )
+
+ 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.get( u"after_login" ) is None
+ assert result[ u"user" ].object_id == self.anonymous.object_id
+
def test_default_after_login( self ):
self.login()
@@ -205,6 +235,72 @@ class Test_root( Test_controller ):
assert u"error" not in result
assert result[ u"notebook" ].object_id == self.privacy_notebook.object_id
+ def test_upgrade( self ):
+ result = self.http_get( "/upgrade" )
+
+ assert result[ u"user" ].username == u"anonymous"
+ assert len( result[ u"notebooks" ] ) == 4
+ assert result[ u"notebooks" ][ 0 ].object_id == self.anon_notebook.object_id
+ assert result[ u"notebooks" ][ 0 ].name == self.anon_notebook.name
+ assert result[ u"notebooks" ][ 0 ].read_write == False
+ assert result[ u"notebooks" ][ 0 ].owner == False
+
+ rate_plan = result[ u"rate_plan" ]
+ assert rate_plan
+ assert rate_plan[ u"name" ] == u"super"
+ assert rate_plan[ u"storage_quota_bytes" ] == 1337
+
+ assert result[ u"notebook" ].object_id == self.anon_notebook.object_id
+ assert len( result[ u"startup_notes" ] ) == 0
+ assert result[ u"note_read_write" ] is False
+
+ assert result[ u"notes" ]
+ assert len( result[ u"notes" ] ) == 1
+ assert result[ u"notes" ][ 0 ].title == u"upgrade your wiki"
+ assert result[ u"notes" ][ 0 ].notebook_id == self.anon_notebook.object_id
+
+ contents = result[ u"notes" ][ 0 ].contents
+ assert u"upgrade" in contents
+ assert u"Super" in contents
+ assert u"Extra super" in contents
+
+ # since the user is not logged in, no subscription buttons should be shown
+ assert u"button" not in contents
+
+ def test_upgrade_after_login( self ):
+ self.login()
+
+ result = self.http_get( "/upgrade", session_id = self.session_id )
+
+ assert result[ u"user" ].username == self.username
+ assert len( result[ u"notebooks" ] ) == 5
+ assert result[ u"notebooks" ][ 0 ].object_id == self.notebook.object_id
+ assert result[ u"notebooks" ][ 0 ].name == self.notebook.name
+ assert result[ u"notebooks" ][ 0 ].read_write == False
+ assert result[ u"notebooks" ][ 0 ].owner == False
+
+ rate_plan = result[ u"rate_plan" ]
+ assert rate_plan
+ assert rate_plan[ u"name" ] == u"super"
+ assert rate_plan[ u"storage_quota_bytes" ] == 1337
+
+ assert result[ u"notebook" ].object_id == self.anon_notebook.object_id
+ assert len( result[ u"startup_notes" ] ) == 0
+ assert result[ u"note_read_write" ] is False
+
+ assert result[ u"notes" ]
+ assert len( result[ u"notes" ] ) == 1
+ assert result[ u"notes" ][ 0 ].title == u"upgrade your wiki"
+ assert result[ u"notes" ][ 0 ].notebook_id == self.anon_notebook.object_id
+
+ contents = result[ u"notes" ][ 0 ].contents
+ assert u"upgrade" in contents
+ assert u"Super" in contents
+ assert u"Extra super" in contents
+
+ # since the user is logged in, subscription buttons should be shown
+ assert u"button" in contents
+
def test_next_id( self ):
result = self.http_get( "/next_id" )
diff --git a/controller/test/Test_users.py b/controller/test/Test_users.py
index 38dc020..1fdf440 100644
--- a/controller/test/Test_users.py
+++ b/controller/test/Test_users.py
@@ -457,6 +457,30 @@ class Test_users( Test_controller ):
assert cherrypy.root.users.check_access( self.user2.object_id, self.notebooks[ 0 ].object_id )
assert cherrypy.root.users.check_access( self.user2.object_id, self.notebooks[ 0 ].trash_id )
+ def test_current_after_login_with_after_login( self ):
+ after_login = u"/foo/bar"
+
+ result = self.http_post( "/users/login", dict(
+ username = self.username2,
+ password = self.password2,
+ after_login = after_login,
+ login_button = u"login",
+ ) )
+
+ assert result[ u"redirect" ] == after_login
+
+ def test_current_after_login_with_after_login_with_full_url( self ):
+ after_login = u"http://this_url/does/not/start/with/a/slash"
+
+ result = self.http_post( "/users/login", dict(
+ username = self.username2,
+ password = self.password2,
+ after_login = after_login,
+ login_button = u"login",
+ ) )
+
+ assert result[ u"redirect" ] == u"/"
+
def test_update_storage( self ):
previous_revision = self.user.revision
diff --git a/static/css/note.css b/static/css/note.css
index 33783ec..2657085 100644
--- a/static/css/note.css
+++ b/static/css/note.css
@@ -138,9 +138,30 @@ ol li {
padding-right: 1em;
}
+#upgrade_table_area {
+ text-align: center;
+}
+
+#upgrade_login_text {
+ font-weight: bold;
+ text-align: center;
+}
+
#upgrade_table {
border-collapse: collapse;
border: 1px solid #999999;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+#upgrade_table th {
+ padding: 0.5em;
+}
+
+#upgrade_table td {
+ text-align: center;
+ background-color: #fafafa;
+ padding: 0.5em;
}
#upgrade_table .plan_name {
@@ -155,19 +176,41 @@ ol li {
background-color: #fafafa;
}
-#upgrade_table .price_text {
+#upgrade_table_small {
+ border-collapse: collapse;
+ border: 1px solid #999999;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+#upgrade_table_small th {
+ padding: 0.5em;
+}
+
+#upgrade_table_small td {
+ text-align: center;
+ background-color: #fafafa;
+ padding: 0.5em;
+}
+
+#upgrade_table_small .plan_name {
+ width: 33%;
+ text-align: center;
+ background-color: #d0e0f0;
+}
+
+.price_text {
color: #ff6600;
}
-#upgrade_table .month_text {
+.month_text {
padding-top: 0.5em;
font-size: 75%;
}
-#upgrade_table td {
- text-align: center;
- background-color: #fafafa;
- padding: 0.5em;
+.subscribe_form {
+ margin-top: 0.5em;
+ margin-bottom: 0;
}
.thumbnail_left {
diff --git a/static/html/faq.html b/static/html/faq.html
index db851dd..1aa8ab0 100644
--- a/static/html/faq.html
+++ b/static/html/faq.html
@@ -16,10 +16,11 @@ whenever you want.
Does this cost me anything?
-Nope, use of your personal Luminotes wiki is completely free. Soon you will
-also be able to upgrade your Luminotes
-account to get notebook sharing features and additional storage space. But the
-features you're using now will always remain free.
+Use of your personal Luminotes wiki is completely free. You also have the
+option of upgrading your Luminotes
+account to get notebook sharing features and additional storage space for a
+reasonable subscription fee. But the features you're using now will always
+remain free.
What does Luminotes run on?
diff --git a/static/html/navigation.html b/static/html/navigation.html
index f802148..3c4533c 100644
--- a/static/html/navigation.html
+++ b/static/html/navigation.html
@@ -8,7 +8,8 @@
faq -
blog -
user guide -
+pricing -
meet the team -
contact info -
-privacy policy
+privacy
diff --git a/static/html/upgrade.html b/static/html/upgrade.html
deleted file mode 100644
index 15c7782..0000000
--- a/static/html/upgrade.html
+++ /dev/null
@@ -1,53 +0,0 @@
-upgrade
-
-
-In a few short weeks, you'll be able to upgrade your Luminotes account to get
-notebook sharing features and additional storage space. Here are some of the
-features you can look forward to.
-
-
-share your notebook
-
-
-
-Most of the time, you want to keep your personal wiki all to yourself. But
-sometimes you simply need to share your work with friends and colleagues. When
-you upgrade your Luminotes account, you'll be able to invite specific people
-to collaborate on your wiki simply by entering their email addresses. You can
-even give them full editing capbilities, so several people can contribute to
-your wiki notebook.
-
-
-access control
-
-
-
-With an upgraded Luminotes wiki, you'll decide exactly how much access to give
-people. Collaborators can make changes to your notebook, while viewers can
-only read your wiki. And owners have the same complete access to your notebook
-that you do. When you're done collaborating, a single click revokes a user's
-notebook access.
-
-
-
-Your wiki access control works on a per-notebook basis, so you can easily
-share one notebook with your friends while keeping your other notebooks
-completely private.
-
-
-additional storage space
-
-
-An upgraded Luminotes account gets you more than just notebook sharing
-features. You'll also be treated to way more room for your personal wiki. That
-means you'll have more space for your notes and ideas, and you won't have to
-worry about running out of room anytime soon.
-
-
-stay tuned
-
-More information about upgrading your Luminotes account will be added as it
-becomes available. Please consider subscribing to the Luminotes blog for updates!
diff --git a/static/js/Wiki.js b/static/js/Wiki.js
index 7eb3660..ee88cea 100644
--- a/static/js/Wiki.js
+++ b/static/js/Wiki.js
@@ -13,7 +13,8 @@ function Wiki( invoker ) {
this.rate_plan = evalJSON( getElement( "rate_plan" ).value );
this.storage_usage_high = false;
this.invites = evalJSON( getElement( "invites" ).value );
- this.invite_id = getElement( "invite_id" ).value ;
+ this.invite_id = getElement( "invite_id" ).value;
+ this.after_login = getElement( "after_login" ).value;
var total_notes_count_node = getElement( "total_notes_count" );
if ( total_notes_count_node )
@@ -652,8 +653,11 @@ 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" || url == "/users/login" ) {
args[ "invite_id" ] = self.invite_id;
+ if ( url == "/users/login" )
+ args[ "after_login" ] = self.after_login;
+ }
self.invoker.invoke( url, "POST", args, callback, form );
} );
diff --git a/tools/initdb.py b/tools/initdb.py
index 453b532..49844ba 100644
--- a/tools/initdb.py
+++ b/tools/initdb.py
@@ -24,7 +24,6 @@ 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 3fa5e97..26afb73 100755
--- a/tools/updatedb.py
+++ b/tools/updatedb.py
@@ -24,7 +24,6 @@ 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/Main_page.py b/view/Main_page.py
index a248ad4..411357a 100644
--- a/view/Main_page.py
+++ b/view/Main_page.py
@@ -31,6 +31,7 @@ class Main_page( Page ):
deleted_id = None,
invites = None,
invite_id = None,
+ after_login = None,
):
startup_note_ids = [ startup_note.object_id for startup_note in startup_notes ]
@@ -102,6 +103,7 @@ class Main_page( Page ):
Input( type = u"hidden", name = u"deleted_id", id = u"deleted_id", value = deleted_id ),
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 ),
Div(
id = u"status_area",
),
diff --git a/view/Notebook_rss.py b/view/Notebook_rss.py
index 1f331d1..9c058ab 100644
--- a/view/Notebook_rss.py
+++ b/view/Notebook_rss.py
@@ -25,6 +25,7 @@ class Notebook_rss( Rss_channel ):
deleted_id = None,
invites = None,
invite_id = None,
+ after_login = None,
):
if notebook.name == u"Luminotes":
notebook_path = u"/"
diff --git a/view/Redeem_invite_note.py b/view/Redeem_invite_note.py
index 104e886..602bfb7 100644
--- a/view/Redeem_invite_note.py
+++ b/view/Redeem_invite_note.py
@@ -3,8 +3,6 @@ from Tags import Span, H3, P, A
class Redeem_invite_note( Span ):
def __init__( self, invite, notebook ):
- title = None
-
Span.__init__(
self,
H3( notebook.name ),
diff --git a/view/Redeem_reset_note.py b/view/Redeem_reset_note.py
index 46215e4..61e93e0 100644
--- a/view/Redeem_reset_note.py
+++ b/view/Redeem_reset_note.py
@@ -3,8 +3,6 @@ from Tags import Span, H3, P, Form, P, Div, Strong, Br, Input
class Redeem_reset_note( Span ):
def __init__( self, password_reset_id, users ):
- title = None
-
Span.__init__(
self,
H3( u"complete your password reset" ),
diff --git a/view/Upgrade_note.py b/view/Upgrade_note.py
new file mode 100644
index 0000000..4e0b342
--- /dev/null
+++ b/view/Upgrade_note.py
@@ -0,0 +1,184 @@
+from Tags import Div, Span, H3, P, A, Table, Tr, Th, Td, Br, Img
+
+
+class Upgrade_note( Span ):
+ def __init__( self, rate_plans, https_url, user_id ):
+ MEGABYTE = 1024 * 1024
+
+ Span.__init__(
+ self,
+ H3( u"upgrade your wiki" ),
+ P(
+ u"When you",
+ A( u"sign up", href = https_url + u"/sign_up", target = u"_top" ),
+ """
+ for a free Luminotes account, you get a full-featured
+ personal wiki available wherever you go. And if you upgrade your
+ Luminotes account, you'll also get powerful notebook sharing features
+ so that you and your friends can all collaborate on your wiki notebook.
+ """,
+ ),
+ P(
+ Table(
+ self.fee_row( rate_plans, user_id ),
+ Tr(
+ Td( u"included storage space", class_ = u"feature_name" ),
+ [ Td(
+ plan[ u"storage_quota_bytes" ] // MEGABYTE, " MB",
+ ) for plan in rate_plans ],
+ ),
+ Tr(
+ Td( u"unlimited wiki notebooks", class_ = u"feature_name" ),
+ [ Td(
+ Img( src = u"/static/images/check.png", width = u"20", height = u"17" ),
+ ) for plan in rate_plans ],
+ ),
+ Tr(
+ Td( u"friendly email support", class_ = u"feature_name" ),
+ [ Td(
+ Img( src = u"/static/images/check.png", width = u"20", height = u"17" ),
+ ) for plan in rate_plans ],
+ ),
+ Tr(
+ Td( u"multi-user collaboration", class_ = u"feature_name" ),
+ [ Td(
+ plan[ u"notebook_collaboration" ] and
+ Img( src = u"/static/images/check.png", width = u"20", height = u"17" ) or u" ",
+ ) for plan in rate_plans ],
+ ),
+ Tr(
+ Td( u"wiki access control", class_ = u"feature_name" ),
+ [ Td(
+ plan[ u"notebook_collaboration" ] and
+ Img( src = u"/static/images/check.png", width = u"20", height = u"17" ) or u" ",
+ ) for plan in rate_plans ],
+ ),
+ border = u"1",
+ id = u"upgrade_table",
+ ),
+ ( not user_id ) and P(
+ u"To upgrade your Luminotes account, please",
+ A( u"login", href = https_url + u"/login?after_login=/upgrade", target = u"_top" ),
+ u"first!",
+ id = u"upgrade_login_text",
+ ) or None,
+ id = u"upgrade_table_area",
+ ),
+
+ H3( u"share your notebook" ),
+ P(
+ A(
+ Img(
+ src = u"/static/images/share_thumb.png",
+ class_ = u"thumbnail_right",
+ width = u"200",
+ height = u"200",
+ ),
+ href = u"/static/images/share.png",
+ target = u"_new",
+ ),
+ u"""
+ Most of the time, you want to keep your personal wiki all to yourself. But
+ sometimes you simply need to share your work with friends and colleagues.
+ """,
+ ),
+ P(
+ u"""
+ With an upgraded Luminotes account, you'll be able to invite specific people
+ to collaborate on your wiki simply by entering their email addresses. You can
+ even give them full editing capbilities, so several people can contribute to
+ your wiki notebook. And you can invite as many people as you want to
+ collaborate on your wiki. They only need to sign up for a free Luminotes
+ account to particpate.
+ """
+ ),
+ H3( u"wiki access control" ),
+ P(
+ A(
+ Img(
+ src = u"/static/images/access_thumb.png",
+ class_ = u"thumbnail_left",
+ width = u"200",
+ height = u"200",
+ ),
+ href = u"/static/images/access.png",
+ target = u"_new",
+ ),
+ u"""
+ With an upgraded Luminotes wiki, you'll decide exactly how much access to give
+ people. Collaborators can make changes to your notebook, while viewers can
+ only read your wiki. And owners have the same complete access to your notebook
+ that you do. When you're done collaborating, a single click revokes a user's
+ notebook access.
+ """,
+ ),
+ P(
+ u"""
+ Your wiki access control works on a per-notebook basis, so you can easily
+ share one notebook with your friends while keeping your other notebooks
+ completely private.
+ """,
+ ),
+ H3( u"additional storage space" ),
+ P(
+ u"""
+ An upgraded Luminotes account gets you more than just notebook sharing
+ features. You'll also be treated to way more room for your personal wiki. That
+ means you'll have more space for your notes and ideas, and you won't have to
+ worry about running out of room anytime soon.
+ """,
+ ),
+ H3( u"no questions asked money-back guarantee" ),
+ P(
+ u"""
+ If you upgrade your Luminotes account and find that it's not meeting your
+ needs, then simply request a refund within 30 days and your money will be
+ returned in full without any questions.
+ """
+ ),
+ P(
+ u"""
+ And no matter how long you've been using an upgraded Luminotes account, you
+ can cancel online anytime. You won't have to send email or talk to anyone in a
+ call center. If you do cancel, you keep all of your wiki notebooks and simply
+ return to a free account.
+ """,
+ ),
+ P(
+ Table(
+ self.fee_row( rate_plans, user_id, include_blank = False ),
+ Tr(
+ [ Td(
+ plan[ u"storage_quota_bytes" ] // MEGABYTE, " MB",
+ ) for plan in rate_plans ],
+ ),
+ border = u"1",
+ id = u"upgrade_table_small",
+ ),
+ ( not user_id ) and P(
+ u"Please",
+ A( u"login", href = https_url + u"/login?after_login=/upgrade", target = u"_top" ),
+ u"to upgrade your wiki!",
+ id = u"upgrade_login_text",
+ ) or None,
+ id = u"upgrade_table_area",
+ ),
+ )
+
+ def fee_row( self, rate_plans, user_id, include_blank = True ):
+ return Tr(
+ include_blank and Th( u" " ) or None,
+ [ Th(
+ plan[ u"name" ].capitalize(),
+ plan[ u"fee" ] and Div(
+ Span(
+ u"$%s" % plan[ u"fee" ],
+ Span( u"/month", class_ = u"month_text" ),
+ class_ = u"price_text",
+ separator = u"",
+ ),
+ user_id and plan.get( u"button" ) % user_id or None,
+ ) or None,
+ class_ = u"plan_name",
+ ) for plan in rate_plans ],
+ )