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 ], + )