Began work on invite redeeming.
This commit is contained in:
parent
687b73d375
commit
8372b03373
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 )
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Reference in New Issue