witten
/
luminotes
Archived
1
0
Fork 0

* Users.signup(), Users.login(), and Root.default() now support optional invite_id parameter.

* Modified Wiki.js to include invite_id parameter when necessary.
 * Increased storage quota (and price) for premium rate plan.
 * Added a note displayed when redeeming an invite, with links to signup and login.
This commit is contained in:
Dan Helfman 2007-12-27 22:16:47 +00:00
parent 8372b03373
commit f00809955c
11 changed files with 262 additions and 28 deletions

View File

@ -41,7 +41,7 @@ settings = {
},
{
"name": "premium",
"storage_quota_bytes": 1000 * MEGABYTE,
"storage_quota_bytes": 2000 * MEGABYTE,
"notebook_collaboration": True,
},
],

View File

@ -45,10 +45,18 @@ class Root( object ):
@expose( Main_page )
@validate(
note_title = unicode,
invite_id = Valid_id( none_okay = True ),
)
def default( self, note_title ):
def default( self, note_title, invite_id = None ):
"""
Convenience method for accessing a note in the main notebook by name rather than by note id.
@type note_title: unicode
@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)
@rtype: unicode
@return: rendered HTML page
"""
# if the user is logged in and not using https, and they request the sign up or login note, then
# redirect to the https version of the page (if available)
@ -56,7 +64,10 @@ class Root( object ):
https_proxy_ip = self.__settings[ u"global" ].get( u"luminotes.https_proxy_ip" )
if note_title in ( u"sign_up", u"login" ) and https_url and cherrypy.request.remote_addr != https_proxy_ip:
return dict( redirect = u"%s/%s" % ( https_url, note_title ) )
if invite_id:
return dict( redirect = u"%s/%s?invite_id=%s" % ( https_url, note_title, invite_id ) )
else:
return dict( redirect = u"%s/%s" % ( https_url, note_title ) )
result = self.__users.current( user_id = None )
first_notebook = result[ u"notebooks" ][ 0 ]
@ -68,6 +79,8 @@ class Root( object ):
raise cherrypy.NotFound
result.update( self.__notebooks.contents( first_notebook.object_id, user_id = user_id, note_id = note.object_id ) )
if invite_id:
result[ "invite_id" ] = invite_id
return result

View File

@ -14,6 +14,7 @@ from Expire import strongly_expire
from view.Json import Json
from view.Main_page import Main_page
from view.Redeem_reset_note import Redeem_reset_note
from view.Redeem_invite_note import Redeem_invite_note
USERNAME_PATTERN = re.compile( "^[a-zA-Z0-9]+$" )
@ -180,8 +181,9 @@ class Users( object ):
password_repeat = Valid_string( min = 1, max = 30 ),
email_address = ( Valid_string( min = 0, max = 60 ) ),
signup_button = unicode,
invite_id = Valid_id( none_okay = True ),
)
def signup( self, username, password, password_repeat, email_address, signup_button ):
def signup( self, username, password, password_repeat, email_address, signup_button, invite_id = None ):
"""
Create a new User based on the given information. Start that user with their own Notebook and a
"welcome to your wiki" Note. For convenience, login the newly created user as well.
@ -196,6 +198,8 @@ class Users( object ):
@param email_address: user's email address
@type signup_button: unicode
@param signup_button: ignored
@type invite_id: unicode
@param invite_id: id of invite to redeem upon signup (optional)
@rtype: json dict
@return: { 'redirect': url, 'authenticated': userdict }
@raise Signup_error: passwords don't match or the username is unavailable
@ -240,7 +244,17 @@ class Users( object ):
self.__database.execute( user.sql_save_notebook( trash_id, read_write = True, owner = True ), commit = False )
self.__database.commit()
redirect = u"/notebooks/%s" % notebook.object_id
# if there's an invite_id, then redeem that invite and redirect to the invite's notebook
if invite_id:
invite = self.__database.load( Invite, invite_id )
if not invite:
raise Signup_error( u"The invite is unknown." )
self.convert_invite_to_access( invite, user_id )
redirect = u"/notebooks/%s" % invite.notebook_id
# otherwise, just redirect to the newly created notebook
else:
redirect = u"/notebooks/%s" % notebook.object_id
return dict(
redirect = redirect,
@ -316,8 +330,9 @@ class Users( object ):
username = ( Valid_string( min = 1, max = 30 ), valid_username ),
password = Valid_string( min = 1, max = 30 ),
login_button = unicode,
invite_id = Valid_id( none_okay = True ),
)
def login( self, username, password, login_button ):
def login( self, username, password, login_button, invite_id = None ):
"""
Attempt to authenticate the user. If successful, associate the given user with the current
session.
@ -326,6 +341,8 @@ class Users( object ):
@param username: username to login
@type password: unicode
@param password: the user's password
@type invite_id: unicode
@param invite_id: id of invite to redeem upon login (optional)
@rtype: json dict
@return: { 'redirect': url, 'authenticated': userdict }
@raise Authentication_error: invalid username or password
@ -338,8 +355,16 @@ class Users( object ):
first_notebook = self.__database.select_one( Notebook, user.sql_load_notebooks( parents_only = True, undeleted_only = True ) )
# redirect to the user's first notebook (if any)
if first_notebook:
# if there's an invite_id, then redeem that invite and redirect to the invite's notebook
if invite_id:
invite = self.__database.load( Invite, invite_id )
if not invite:
raise Authentication_error( u"The invite is unknown." )
self.convert_invite_to_access( invite, user.object_id )
redirect = u"/notebooks/%s" % invite.notebook_id
# otherwise, just redirect to the user's first notebook (if any)
elif first_notebook:
redirect = u"/notebooks/%s" % first_notebook.object_id
else:
redirect = u"/"
@ -741,7 +766,7 @@ class Users( object ):
# if the invite is already redeemed, then update the relevant entry in the user_notebook
# access table as well
if similar.redeemed_user_id is not None:
redeemed_user = self.__database.load( User, redeemed_user_id )
redeemed_user = self.__database.load( User, similar.redeemed_user_id )
if redeemed_user:
self.__database.execute( redeemed_user.sql_update_access( notebook_id, read_write, owner ) )
@ -833,8 +858,8 @@ class Users( object ):
@param invite_id: id of invite to redeem
@type user_id: unicode
@param user_id: id of current logged-in user (if any), determined by @grab_user_id
@rtype:
@return:
@rtype: unicode
@return: rendered HTML page
@raise Validation_error: one of the arguments is invalid
@raise Invite_error: an error occured when redeeming the invite
"""
@ -857,7 +882,32 @@ class Users( object ):
if invite.redeemed_user_id:
raise Invite_error( u"That invite has already been used. If you were the one who used it, then simply <a href=\"/login\">login</a> to your account." )
# TODO: give the user the option to sign up or login in order to redeem the invite
notebook = self.__database.load( Notebook, invite.notebook_id )
if not notebook:
raise Invite_error( "That notebook you've been invited to is unknown." )
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 ) )
invite_notebook = self.__database.load( Notebook, invite.notebook_id )
if not anonymous or not main_notebook or not invite_notebook:
raise Password_reset_error( "There was an error when redeeming your invite. Please contact %s." % self.__support_email )
# give the user the option to sign up or login in order to redeem the invite
result = self.current( anonymous.object_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"redeem_invite",
contents = unicode( Redeem_invite_note( invite, invite_notebook ) ),
notebook_id = main_notebook.object_id,
) ]
result[ "invites" ] = []
return result
def convert_invite_to_access( self, invite, user_id ):
"""

View File

@ -1,4 +1,5 @@
from copy import copy
from model.User import User
class Stub_database( object ):
@ -7,10 +8,13 @@ class Stub_database( object ):
self.objects = {}
self.user_notebook = {} # map of user_id to ( notebook_id, read_write, owner )
self.last_saved_obj = None
self.last_saved_user = None
self.__next_id = 0
def save( self, obj, commit = False ):
self.last_saved_obj = obj
if isinstance( obj, User ):
self.last_saved_user = obj
if obj.object_id in self.objects:
self.objects[ obj.object_id ].append( copy( obj ) )
else:

View File

@ -103,6 +103,18 @@ class Test_root( Test_controller ):
assert result[ u"notes" ][ 0 ].object_id == self.anon_note.object_id
assert result[ u"notebook" ].object_id == self.anon_notebook.object_id
def test_default_with_invite_id( self ):
result = self.http_get(
"/my_note?invite_id=whee",
)
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"invite_id" ] == u"whee"
def test_default_with_unknown_note( self ):
result = self.http_get(
"/unknown_note",

View File

@ -44,6 +44,9 @@ class Test_users( Test_controller ):
trash_id1 = self.database.next_id( Notebook )
trash_id2 = self.database.next_id( Notebook )
self.database.save( Notebook.create( trash_id1, u"trash" ) )
self.database.save( Notebook.create( trash_id2, u"trash" ) )
self.notebooks = [
Notebook.create( notebook_id1, u"my notebook", trash_id = trash_id1 ),
Notebook.create( notebook_id2, u"my other notebook", trash_id = trash_id2 ),
@ -158,6 +161,83 @@ class Test_users( Test_controller ):
assert rate_plan[ u"name" ] == u"super"
assert rate_plan[ u"storage_quota_bytes" ] == 1337
def test_current_after_signup_with_invite_id( self ):
# trick send_invites() into using a fake SMTP server
Stub_smtp.reset()
smtplib.SMTP = Stub_smtp
self.login()
self.user.rate_plan = 1
self.database.save( self.user )
email_addresses_list = [ u"foo@example.com" ]
email_addresses = email_addresses_list[ 0 ]
result = self.http_post( "/users/send_invites", dict(
notebook_id = self.notebooks[ 0 ].object_id,
email_addresses = email_addresses,
access = u"viewer",
invite_button = u"send invites",
), session_id = self.session_id )
matches = self.INVITE_LINK_PATTERN.search( smtplib.SMTP.message )
invite_id = matches.group( 2 )
result = self.http_post( "/users/signup", dict(
username = self.new_username,
password = self.new_password,
password_repeat = self.new_password,
email_address = self.new_email_address,
signup_button = u"sign up",
invite_id = invite_id,
) )
invite_notebook_id = result[ u"redirect" ].split( u"/notebooks/" )[ -1 ]
assert invite_notebook_id == self.notebooks[ 0 ].object_id
user = self.database.last_saved_user
assert isinstance( user, User )
result = cherrypy.root.users.current( user.object_id )
assert result[ u"user" ].object_id == user.object_id
assert result[ u"user" ].username == self.new_username
assert result[ u"user" ].email_address == self.new_email_address
assert cherrypy.root.users.check_access( user.object_id, self.notebooks[ 0 ].object_id )
assert cherrypy.root.users.check_access( user.object_id, self.notebooks[ 0 ].trash_id )
# the notebook that the user was invited to should be in the list of returned notebooks
notebooks = dict( [ ( notebook.object_id, notebook ) for notebook in result[ u"notebooks" ] ] )
notebook = notebooks.get( invite_notebook_id )
assert notebook
assert notebook.revision
assert notebook.name == self.notebooks[ 0 ].name
assert notebook.trash_id
assert notebook.read_write == False
assert notebook.owner == False
notebook = notebooks.get( self.notebooks[ 0 ].trash_id )
assert notebook.revision
assert notebook.name == u"trash"
assert notebook.trash_id == None
assert notebook.read_write == False
assert notebook.owner == False
notebook = notebooks.get( self.anon_notebook.object_id )
assert notebook.revision == self.anon_notebook.revision
assert notebook.name == self.anon_notebook.name
assert notebook.trash_id == None
assert notebook.read_write == False
assert notebook.owner == False
assert result.get( u"login_url" ) is None
assert result[ u"logout_url" ] == self.settings[ u"global" ][ u"luminotes.https_url" ] + u"/"
rate_plan = result[ u"rate_plan" ]
assert rate_plan[ u"name" ] == u"super"
assert rate_plan[ u"storage_quota_bytes" ] == 1337
def test_signup_with_different_passwords( self ):
result = self.http_post( "/users/signup", dict(
username = self.new_username,
@ -329,6 +409,41 @@ class Test_users( Test_controller ):
assert rate_plan[ u"name" ] == u"super"
assert rate_plan[ u"storage_quota_bytes" ] == 1337
def test_current_after_login_with_invite_id( self ):
# trick send_invites() into using a fake SMTP server
Stub_smtp.reset()
smtplib.SMTP = Stub_smtp
self.login()
self.user.rate_plan = 1
self.database.save( self.user )
email_addresses_list = [ u"foo@example.com" ]
email_addresses = email_addresses_list[ 0 ]
result = self.http_post( "/users/send_invites", dict(
notebook_id = self.notebooks[ 0 ].object_id,
email_addresses = email_addresses,
access = u"viewer",
invite_button = u"send invites",
), session_id = self.session_id )
matches = self.INVITE_LINK_PATTERN.search( smtplib.SMTP.message )
invite_id = matches.group( 2 )
result = self.http_post( "/users/login", dict(
username = self.username2,
password = self.password2,
invite_id = invite_id,
login_button = u"login",
) )
invite_notebook_id = result[ u"redirect" ].split( u"/notebooks/" )[ -1 ]
assert invite_notebook_id == self.notebooks[ 0 ].object_id
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_update_storage( self ):
previous_revision = self.user.revision
@ -1614,3 +1729,11 @@ class Test_users( Test_controller ):
login_button = u"login",
) )
self.session_id = result[ u"session_id" ]
def login2( self ):
result = self.http_post( "/users/login", dict(
username = self.username2,
password = self.password2,
login_button = u"login",
) )
self.session_id = result[ u"session_id" ]

View File

@ -6,13 +6,13 @@
<th class="plan_name">Free</th>
<th class="plan_name">Basic<br /><span class="price_text">$5<span class="month_text">/month</span></span></th>
<th class="plan_name">Standard<br /><span class="price_text">$9<span class="month_text">/month</span></span></th>
<th class="plan_name">Premium<br /><span class="price_text">$14<span class="month_text">/month</span></span></th>
<th class="plan_name">Premium<br /><span class="price_text">$19<span class="month_text">/month</span></span></th>
<tr>
<td class="feature_name">included storage space</td>
<td>30 MB</td>
<td>250 MB</td>
<td>500 MB</td>
<td>1000 MB</td>
<td>2000 MB</td>
</tr>
<tr>
<td class="feature_name">unlimited wiki notebooks</td>

View File

@ -13,6 +13,7 @@ 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 ;
var total_notes_count_node = getElement( "total_notes_count" );
if ( total_notes_count_node )
@ -642,7 +643,11 @@ Wiki.prototype.create_editor = function ( id, note_text, deleted_from_id, revisi
connect( editor, "hide_clicked", function ( event ) { self.hide_editor( event, editor ) } );
connect( editor, "invites_updated", function ( invites ) { self.invites = invites; self.share_notebook(); } );
connect( editor, "submit_form", function ( url, form, callback ) {
self.invoker.invoke( url, "POST", null, callback, form );
var args = {}
if ( url == "/users/signup" || url == "/users/login" )
args[ "invite_id" ] = self.invite_id;
self.invoker.invoke( url, "POST", args, callback, form );
} );
connect( editor, "revoke_invite", function ( invite_id, callback ) {
self.invoker.invoke( "/users/revoke_invite", "POST", {
@ -1346,23 +1351,20 @@ Wiki.prototype.display_invites = function ( invite_area ) {
"title": "revoke this person's notebook access"
} );
var add_invite_to = null;
if ( invite.owner ) {
appendChildNodes(
owners, createDOM( "div", { "class": "invite" },
invite.email_address, " ", revoke_button )
);
add_invite_to = owners;
} else {
if ( invite.read_write )
appendChildNodes(
collaborators, createDOM( "div", { "class": "invite" },
invite.email_address, " ", revoke_button )
);
add_invite_to = collaborators;
else
appendChildNodes(
viewers, createDOM( "div", { "class": "invite" },
invite.email_address, " ", revoke_button )
);
add_invite_to = viewers;
}
appendChildNodes(
add_invite_to, createDOM( "div", { "class": "invite" },
invite.email_address, " ", revoke_button )
);
}
var div = createDOM( "div" );

View File

@ -4,7 +4,11 @@ from Rounded_div import Rounded_div
class Link_area( Div ):
def __init__( self, notebooks, notebook, total_notes_count, parent_id, notebook_path, user ):
linked_notebooks = [ nb for nb in notebooks if nb.read_write and nb.name not in ( u"trash" ) and nb.deleted is False ]
linked_notebooks = [ nb for nb in notebooks if
( nb.read_write or not nb.name.startswith( u"Luminotes" ) ) and
nb.name not in ( u"trash" ) and
nb.deleted is False
]
Div.__init__(
self,

View File

@ -30,6 +30,7 @@ class Main_page( Page ):
rename = False,
deleted_id = None,
invites = None,
invite_id = None,
):
startup_note_ids = [ startup_note.object_id for startup_note in startup_notes ]
@ -100,6 +101,7 @@ class Main_page( Page ):
Input( type = u"hidden", name = u"rename", id = u"rename", value = json( rename ) ),
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 ),
Div(
id = u"status_area",
),

View File

@ -0,0 +1,24 @@
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 ),
P(
u"You are just seconds away from viewing \"%s\"." % notebook.name,
),
P(
u"If you already have a Luminotes account, then simply ",
A( u"login", href = u"/login?invite_id=%s" % invite.object_id, target = "_top" ),
u" to your account."
),
P(
u"Otherwise, please ",
A( u"sign up", href = u"/sign_up?invite_id=%s" % invite.object_id, target = "_top" ),
u" for a free account."
),
)