witten
/
luminotes
Archived
1
0
Fork 0

Began work on invite redeeming.

This commit is contained in:
Dan Helfman 2007-12-18 00:05:13 +00:00
parent 687b73d375
commit 8372b03373
9 changed files with 239 additions and 6 deletions

View File

@ -55,6 +55,7 @@ def expose( view = None, rss = None ):
except Exception, error:
original_error = error
if hasattr( error, "to_dict" ):
if not view: raise error
result = error.to_dict()
else:
import traceback

View File

@ -88,6 +88,23 @@ class Root( object ):
redirect = u"/users/redeem_reset/%s" % password_reset_id,
)
@expose()
def i( self, invite_id ):
"""
Redirect to the invite redemption URL, based on the given invite id. The sole purpose of this
method is to shorten invite redemption URLs sent by email so email clients don't wrap them.
"""
# if the value looks like an id, it's an invite id, so redirect
try:
validator = Valid_id()
invite_id = validator( invite_id )
except ValueError:
raise cherrypy.NotFound
return dict(
redirect = u"/users/redeem_invite/%s" % invite_id,
)
@expose( view = Main_page )
@strongly_expire
@grab_user_id

View File

@ -738,6 +738,13 @@ class Users( object ):
similar.owner = owner
self.__database.save( similar, commit = False )
# 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 )
if redeemed_user:
self.__database.execute( redeemed_user.sql_update_access( notebook_id, read_write, owner ) )
# create an email message with a unique invitation link
notebook_name = notebook.name.strip().replace( "\n", " " ).replace( "\r", " " )
message = Message.Message()
@ -811,3 +818,72 @@ class Users( object ):
message = u"Notebook access for %s has been revoked." % invite.email_address,
invites = invites,
)
@expose( view = Main_page )
@grab_user_id
@validate(
invite_id = Valid_id(),
user_id = Valid_id( none_okay = True ),
)
def redeem_invite( self, invite_id, user_id = None ):
"""
Begin the process of redeeming a notebook invite.
@type invite_id: unicode
@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:
@raise Validation_error: one of the arguments is invalid
@raise Invite_error: an error occured when redeeming the invite
"""
invite = self.__database.load( Invite, invite_id )
if not invite:
raise Invite_error( "That invite is unknown. Please make sure that you typed the address correctly." )
if user_id is not None:
# if the user is logged in but the invite is unredeemed, redeem it and redirect to the notebook
if invite.redeemed_user_id is None:
self.convert_invite_to_access( invite, user_id )
return dict( redirect = u"/notebooks/%s" % invite.notebook_id )
# if the user is logged in and has already redeemed this invite, then just redirect to the notebook
if invite.redeemed_user_id == user_id:
return dict( redirect = u"/notebooks/%s" % invite.notebook_id )
else:
raise Invite_error( u"That invite has already been used by someone else." )
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
def convert_invite_to_access( self, invite, user_id ):
"""
Grant the given user access to the notebook specified in the invite, and mark that invite as
redeemed.
@type invite: model.Invite
@param invite: invite to convert to notebook access
@type user_id: unicode
@param user_id: id of current logged-in user (if any), determined by @grab_user_id
@raise Invite_error: an error occured when redeeming the invite
"""
user = self.__database.load( User, user_id )
notebook = self.__database.load( Notebook, invite.notebook_id )
if not user or not notebook:
raise Invite_error( "There was an error when redeeming your invite. Please contact %s." % self.__support_email )
# if the user doesn't already have access to this notebook, then grant access
if not self.__database.select_one( bool, user.sql_has_access( notebook.object_id ) ):
self.__database.execute( user.sql_save_notebook( notebook.object_id, invite.read_write, invite.owner ), commit = False )
# the same goes for the trash notebook
if not self.__database.select_one( bool, user.sql_has_access( notebook.trash_id ) ):
self.__database.execute( user.sql_save_notebook( notebook.trash_id, invite.read_write, invite.owner ), commit = False )
invite.redeemed_user_id = user_id
self.__database.save( invite, commit = False )
self.__database.commit()

View File

@ -5,7 +5,7 @@ class Stub_database( object ):
def __init__( self, connection = None ):
# map of object id to list of saved objects (presumably in increasing order of revisions)
self.objects = {}
self.user_notebook = {} # map of user_id to ( notebook_id, read_write )
self.user_notebook = {} # map of user_id to ( notebook_id, read_write, owner )
self.last_saved_obj = None
self.__next_id = 0

View File

@ -221,7 +221,7 @@ class Test_controller( object ):
for ( object_id, obj_list ) in database.objects.items():
obj = obj_list[ -1 ]
if isinstance( obj, Invite ) and obj.notebook_id == self.notebook_id and \
obj.email_address == self.email_address and obj.redeemed_user_id is None and \
obj.email_address == self.email_address and \
obj.object_id != self.object_id:
invites.append( obj )

View File

@ -213,3 +213,9 @@ class Test_root( Test_controller ):
result = self.http_get( "/r/%s" % redeem_reset_id )
assert result[ u"redirect" ] == u"/users/redeem_reset/%s" % redeem_reset_id
def test_redeem_invite( self ):
invite_id = u"foobarbaz"
result = self.http_get( "/i/%s" % invite_id )
assert result[ u"redirect" ] == u"/users/redeem_invite/%s" % invite_id

View File

@ -4,14 +4,14 @@ import smtplib
from pytz import utc
from nose.tools import raises
from datetime import datetime, timedelta
from nose.tools import raises
from Test_controller import Test_controller
from Stub_smtp import Stub_smtp
from model.User import User
from model.Notebook import Notebook
from model.Note import Note
from model.Password_reset import Password_reset
from controller.Users import Access_error
from model.Invite import Invite
from controller.Users import Invite_error
class Test_users( Test_controller ):
@ -1001,6 +1001,13 @@ class Test_users( Test_controller ):
invite_id1 = matches.group( 2 )
assert invite_id1
# update the user_notebook table accordingly. this normally happens when an invite is redeemed
self.database.execute( self.user.sql_save_notebook(
self.notebooks[ 0 ].object_id,
read_write = False,
owner = False,
) )
# then send a similar invite to the same email address with read_write and owner set to True
result = self.http_post( "/users/send_invites", dict(
notebook_id = self.notebooks[ 0 ].object_id,
@ -1037,6 +1044,14 @@ class Test_users( Test_controller ):
assert invite2.read_write is True
assert invite2.owner is True
# assert that the user_notebook table has also been updated accordingly
access = self.database.select_one( bool, self.user.sql_has_access(
self.notebooks[ 0 ].object_id,
read_write = True,
owner = True,
) )
assert access is True
def test_send_invites_with_generic_from_address( self ):
Stub_smtp.reset()
smtplib.SMTP = Stub_smtp
@ -1483,6 +1498,115 @@ class Test_users( Test_controller ):
assert result[ u"error" ]
assert "access" in result[ u"error" ]
def test_convert_invite_to_access( 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 ]
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 )
invite = self.database.load( Invite, invite_id )
cherrypy.root.users.convert_invite_to_access( invite, self.user.object_id )
access = self.database.select_one( bool, self.user.sql_has_access(
invite.notebook_id,
invite.read_write,
invite.owner,
) )
assert access is True
notebook = self.database.load( Notebook, invite.notebook_id )
access = self.database.select_one( bool, self.user.sql_has_access(
notebook.trash_id,
invite.read_write,
invite.owner,
) )
assert access is True
assert invite.redeemed_user_id == self.user.object_id
def test_convert_invite_to_access_twice( self ):
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 ]
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 )
invite = self.database.load( Invite, invite_id )
cherrypy.root.users.convert_invite_to_access( invite, self.user.object_id )
cherrypy.root.users.convert_invite_to_access( invite, self.user.object_id )
access = self.database.select_one( bool, self.user.sql_has_access(
invite.notebook_id,
invite.read_write,
invite.owner,
) )
assert access is True
notebook = self.database.load( Notebook, invite.notebook_id )
access = self.database.select_one( bool, self.user.sql_has_access(
notebook.trash_id,
invite.read_write,
invite.owner,
) )
assert access is True
assert invite.redeemed_user_id == self.user.object_id
@raises( Invite_error )
def test_convert_invite_with_unknown_user( self ):
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 ]
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 )
invite = self.database.load( Invite, invite_id )
cherrypy.root.users.convert_invite_to_access( invite, u"unknown_user_id" )
def login( self ):
result = self.http_post( "/users/login", dict(
username = self.username,

View File

@ -91,9 +91,9 @@ class Invite( Persistent ):
quote( self.__redeemed_user_id ), quote( self.object_id ) )
def sql_load_similar( self ):
# select unredeemed invites with the same notebook_id, and email_address as this invite
# select invites with the same notebook_id, and email_address as this invite
return "select id, revision, from_user_id, notebook_id, email_address, read_write, owner, redeemed_user_id from invite " + \
"where notebook_id = %s and email_address = %s and id != %s and redeemed_user_id is null;" % \
"where notebook_id = %s and email_address = %s and id != %s;" % \
( quote( self.__notebook_id ), quote( self.__email_address ), quote( self.object_id ) )
@staticmethod

View File

@ -185,6 +185,15 @@ class User( Persistent ):
"select user_id from user_notebook where user_id = %s and notebook_id = %s;" % \
( quote( self.object_id ), quote( notebook_id ) )
def sql_update_access( self, notebook_id, read_write = False, owner = False ):
"""
Return a SQL string to update the user's notebook access to the given read_write and owner level.
"""
return \
"update user_notebook set read_write = %s, owner = %s where user_id = %s and notebook_id = %s;" % \
( quote( read_write and 't' or 'f' ), quote( owner and 't' or 'f' ), quote( self.object_id ),
quote( notebook_id ) )
def sql_calculate_storage( self ):
"""
Return a SQL string to calculate the total bytes of storage usage by this user. Note that this