witten
/
luminotes
Archived
1
0
Fork 0

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.
This commit is contained in:
Dan Helfman 2008-01-10 08:33:19 +00:00
parent 08cd7057f3
commit 8f00cceb94
19 changed files with 442 additions and 81 deletions

2
NEWS
View File

@ -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.

View File

@ -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,
# },
],
},
}

View File

@ -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 ):

View File

@ -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

View File

@ -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",
},
],
},

View File

@ -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" )

View File

@ -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

View File

@ -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 {

View File

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

View File

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

View File

@ -1,53 +0,0 @@
<h3>upgrade</h3>
<p>
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.
</p>
<h3>share your notebook</h3>
<p>
<a href="/static/images/share.png" target="_new"><img
src="/static/images/share_thumb.png" class="thumbnail_right" width="200" height="200" /></a>
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.
</p>
<h3>access control</h3>
<p>
<a href="/static/images/access.png" target="_new"><img
src="/static/images/access_thumb.png" class="thumbnail_left" width="200" height="200" /></a>
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>
<p>
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.
</p>
<h3>additional storage space</h3>
<p>
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.
</p>
<h3>stay tuned</h3>
More information about upgrading your Luminotes account will be added as it
becomes available. Please consider subscribing to the <a href="/blog"
target="_top">Luminotes blog</a> for updates!

View File

@ -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 );
} );

View File

@ -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 ):

View File

@ -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 ):

View File

@ -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",
),

View File

@ -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"/"

View File

@ -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 ),

View File

@ -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" ),

184
view/Upgrade_note.py Normal file
View File

@ -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"&nbsp",
) 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"&nbsp",
) 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"&nbsp;" ) 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 ],
)