diff --git a/controller/Notebooks.py b/controller/Notebooks.py index 4e938bb..867f99f 100644 --- a/controller/Notebooks.py +++ b/controller/Notebooks.py @@ -9,6 +9,7 @@ from Expire import strongly_expire from Html_nuker import Html_nuker from model.Notebook import Notebook from model.Note import Note +from model.Invite import Invite from model.User import User from view.Main_page import Main_page from view.Json import Json @@ -152,12 +153,14 @@ class Notebooks( object ): startup_notes = self.__database.select_many( Note, notebook.sql_load_startup_notes() ) total_notes_count = self.__database.select_one( int, notebook.sql_count_notes() ) + invites = self.__database.select_many( Invite, Invite.sql_load_notebook_invites( notebook_id ) ) return dict( notebook = notebook, startup_notes = startup_notes, total_notes_count = total_notes_count, notes = note and [ note ] or [], + invites = invites or [], ) @expose( view = Json ) diff --git a/controller/Root.py b/controller/Root.py index 3c90a12..7572a2d 100644 --- a/controller/Root.py +++ b/controller/Root.py @@ -240,9 +240,9 @@ class Root( object ): from email import Message message = Message.Message() - message[ u"from" ] = support_email - message[ u"to" ] = support_email - message[ u"subject" ] = u"Luminotes traceback" + message[ u"From" ] = support_email + message[ u"To" ] = support_email + message[ u"Subject" ] = u"Luminotes traceback" message.set_payload( u"requested URL: %s\n" % cherrypy.request.browser_url + u"user id: %s\n" % cherrypy.session.get( "user_id" ) + @@ -253,7 +253,7 @@ class Root( object ): # send the message out through localhost's smtp server server = smtplib.SMTP() server.connect() - server.sendmail( message[ u"from" ], [ support_email ], message.as_string() ) + server.sendmail( message[ u"From" ], [ support_email ], message.as_string() ) server.quit() return True diff --git a/controller/Users.py b/controller/Users.py index fee5239..79ca044 100644 --- a/controller/Users.py +++ b/controller/Users.py @@ -578,6 +578,7 @@ class Users( object ): contents = unicode( Redeem_reset_note( password_reset_id, matching_users ) ), notebook_id = main_notebook.object_id, ) ] + result[ "invites" ] = [] return result diff --git a/controller/test/Test_controller.py b/controller/test/Test_controller.py index b34946e..37cadaa 100644 --- a/controller/test/Test_controller.py +++ b/controller/test/Test_controller.py @@ -230,6 +230,20 @@ class Test_controller( object ): Invite.sql_load_similar = lambda self: \ lambda database: sql_load_similar( self, database ) + def sql_load_notebook_invites( notebook_id, database ): + invites = [] + + for ( object_id, obj_list ) in database.objects.items(): + obj = obj_list[ -1 ] + if isinstance( obj, Invite ) and obj.notebook_id == notebook_id and \ + obj.email_address not in [ i.email_address for i in invites ]: + invites.append( obj ) + + return invites + + Invite.sql_load_notebook_invites = staticmethod( lambda notebook_id: + lambda database: sql_load_notebook_invites( notebook_id, database ) ) + def setUp( self ): from controller.Root import Root cherrypy.lowercase_api = True diff --git a/controller/test/Test_notebooks.py b/controller/test/Test_notebooks.py index 69332eb..c681fd4 100644 --- a/controller/test/Test_notebooks.py +++ b/controller/test/Test_notebooks.py @@ -6,6 +6,7 @@ from Test_controller import Test_controller from model.Notebook import Notebook from model.Note import Note from model.User import User +from model.Invite import Invite from controller.Notebooks import Access_error @@ -21,6 +22,7 @@ class Test_notebooks( Test_controller ): self.password = u"trustno1" self.email_address = u"outthere@example.com" self.user = None + self.invite = None self.anonymous = None self.session_id = None @@ -55,6 +57,12 @@ class Test_notebooks( Test_controller ): self.database.save( self.anonymous, commit = False ) self.database.execute( self.user.sql_save_notebook( self.anon_notebook.object_id, read_write = False, owner = False ) ) + self.invite = Invite.create( + self.database.next_id( Invite ), self.user.object_id, self.notebook.object_id, + u"skinner@example.com", read_write = True, owner = False, + ) + self.database.save( self.invite, commit = False ) + def test_default_without_login( self ): result = self.http_get( "/notebooks/%s" % self.notebook.object_id, @@ -84,6 +92,11 @@ class Test_notebooks( Test_controller ): assert result.get( u"parent_id" ) == None assert result.get( u"note_read_write" ) in ( None, True ) + invites = result[ "invites" ] + assert len( invites ) == 1 + invite = invites[ 0 ] + assert invite.object_id == self.invite.object_id + user = self.database.load( User, self.user.object_id ) assert user.storage_bytes == 0 @@ -110,6 +123,11 @@ class Test_notebooks( Test_controller ): assert result.get( u"parent_id" ) == None assert result.get( u"note_read_write" ) in ( None, True ) + invites = result[ "invites" ] + assert len( invites ) == 1 + invite = invites[ 0 ] + assert invite.object_id == self.invite.object_id + user = self.database.load( User, self.user.object_id ) assert user.storage_bytes == 0 @@ -141,6 +159,11 @@ class Test_notebooks( Test_controller ): assert result.get( u"parent_id" ) == None assert result.get( u"note_read_write" ) == False + invites = result[ "invites" ] + assert len( invites ) == 1 + invite = invites[ 0 ] + assert invite.object_id == self.invite.object_id + user = self.database.load( User, self.user.object_id ) assert user.storage_bytes == 0 @@ -165,6 +188,11 @@ class Test_notebooks( Test_controller ): assert result.get( u"parent_id" ) == parent_id assert result.get( u"note_read_write" ) in ( None, True ) + invites = result[ "invites" ] + assert len( invites ) == 1 + invite = invites[ 0 ] + assert invite.object_id == self.invite.object_id + user = self.database.load( User, self.user.object_id ) assert user.storage_bytes == 0 @@ -179,6 +207,11 @@ class Test_notebooks( Test_controller ): assert result[ "total_notes_count" ] == 2 assert result[ "notes" ] == [] + invites = result[ "invites" ] + assert len( invites ) == 1 + invite = invites[ 0 ] + assert invite.object_id == self.invite.object_id + assert notebook.object_id == self.notebook.object_id assert notebook.read_write == True assert notebook.owner == True @@ -198,6 +231,11 @@ class Test_notebooks( Test_controller ): startup_notes = result[ "startup_notes" ] assert result[ "total_notes_count" ] == 2 + invites = result[ "invites" ] + assert len( invites ) == 1 + invite = invites[ 0 ] + assert invite.object_id == self.invite.object_id + assert notebook.object_id == self.notebook.object_id assert notebook.read_write == True assert notebook.owner == True @@ -226,6 +264,11 @@ class Test_notebooks( Test_controller ): startup_notes = result[ "startup_notes" ] assert result[ "total_notes_count" ] == 2 + invites = result[ "invites" ] + assert len( invites ) == 1 + invite = invites[ 0 ] + assert invite.object_id == self.invite.object_id + assert notebook.object_id == self.notebook.object_id assert notebook.read_write == True assert notebook.owner == True @@ -242,6 +285,71 @@ class Test_notebooks( Test_controller ): user = self.database.load( User, self.user.object_id ) assert user.storage_bytes == 0 + def test_contents_with_different_invites( self ): + # create an invite with a different email address from the previous + invite = Invite.create( + self.database.next_id( Invite ), self.user.object_id, self.notebook.object_id, + u"smoking@example.com", read_write = True, owner = False, + ) + self.database.save( invite ) + + result = cherrypy.root.notebooks.contents( + notebook_id = self.notebook.object_id, + user_id = self.user.object_id, + ) + + notebook = result[ "notebook" ] + startup_notes = result[ "startup_notes" ] + assert result[ "total_notes_count" ] == 2 + assert result[ "notes" ] == [] + + invites = result[ "invites" ] + assert len( invites ) == 2 + invite = invites[ 0 ] + assert invite.object_id == invite.object_id + invite = invites[ 1 ] + assert invite.object_id == self.invite.object_id + + assert notebook.object_id == self.notebook.object_id + assert notebook.read_write == True + assert notebook.owner == True + assert len( startup_notes ) == 1 + assert startup_notes[ 0 ].object_id == self.note.object_id + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes == 0 + + def test_contents_with_duplicate_invites( self ): + # create an invite with the same email address as the previous invite + invite = Invite.create( + self.database.next_id( Invite ), self.user.object_id, self.notebook.object_id, + u"skinner@example.com", read_write = True, owner = False, + ) + self.database.save( invite ) + + result = cherrypy.root.notebooks.contents( + notebook_id = self.notebook.object_id, + user_id = self.user.object_id, + ) + + notebook = result[ "notebook" ] + startup_notes = result[ "startup_notes" ] + assert result[ "total_notes_count" ] == 2 + assert result[ "notes" ] == [] + + # the two invites should be collapsed down into one + invites = result[ "invites" ] + assert len( invites ) == 1 + invite = invites[ 0 ] + assert invite.object_id == invite.object_id + + assert notebook.object_id == self.notebook.object_id + assert notebook.read_write == True + assert notebook.owner == True + assert len( startup_notes ) == 1 + assert startup_notes[ 0 ].object_id == self.note.object_id + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes == 0 + @raises( Access_error ) def test_contents_without_user_id( self ): result = cherrypy.root.notebooks.contents( @@ -272,6 +380,7 @@ class Test_notebooks( Test_controller ): startup_notes = result[ "startup_notes" ] assert result[ "notes" ] == [] assert result[ "total_notes_count" ] == 0 + assert result[ "invites" ] == [] assert notebook.object_id == self.anon_notebook.object_id assert notebook.read_write == False @@ -1762,6 +1871,7 @@ class Test_notebooks( Test_controller ): assert result[ "total_notes_count" ] == 0 assert result[ "startup_notes" ] == [] assert result[ "notes" ] == [] + assert result[ "invites" ] == [] assert notebook.object_id == new_notebook_id assert notebook.read_write == True diff --git a/model/Invite.py b/model/Invite.py index 02d8bce..20f8068 100644 --- a/model/Invite.py +++ b/model/Invite.py @@ -91,11 +91,31 @@ class Invite( Persistent ): quote( self.__redeemed_user_id ), quote( self.object_id ) ) def sql_load_similar( self ): - # select unredeemed invitations with the same from_user_id, notebook_id, and email_address as this invitation + # select unredeemed invites with the same from_user_id, 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 from_user_id = %s and notebook_id = %s and email_address = %s and id != %s and redeemed_user_id is null;" % \ ( quote( self.__from_user_id ), quote( self.__notebook_id ), quote( self.__email_address ), quote( self.object_id ) ) + @staticmethod + def sql_load_notebook_invites( notebook_id ): + # select a list of invites to the given notebook. if there are multiple invites for a given + # email_address, arbitrarily pick one of them + return "select id, revision, from_user_id, notebook_id, email_address, read_write, owner, redeemed_user_id from invite " + \ + "where id in ( select max( id ) from invite where notebook_id = %s group by email_address ) order by email_address;" % quote( notebook_id ) + + def to_dict( self ): + d = Persistent.to_dict( self ) + d.update( dict( + from_user_id = self.__from_user_id, + notebook_id = self.__notebook_id, + email_address = self.__email_address, + read_write = self.__read_write, + owner = self.__owner, + redeemed_user_id = self.__redeemed_user_id, + ) ) + + return d + def __set_read_write( self, read_write ): if read_write != self.__read_write: self.update_revision() diff --git a/model/test/Test_invite.py b/model/test/Test_invite.py index 0392c47..0847c73 100644 --- a/model/test/Test_invite.py +++ b/model/test/Test_invite.py @@ -1,3 +1,5 @@ +from pytz import utc +from datetime import datetime, timedelta from model.User import User from model.Invite import Invite @@ -10,6 +12,7 @@ class Test_invite( object ): self.email_address = u"bob@example.com" self.read_write = True self.owner = False + self.delta = timedelta( seconds = 1 ) self.invite = Invite.create( self.object_id, self.from_user_id, self.notebook_id, self.email_address, self.read_write, self.owner ) @@ -39,3 +42,14 @@ class Test_invite( object ): assert self.invite.redeemed_user_id == redeemed_user_id assert self.invite.revision == current_revision + + def test_to_dict( self ): + d = self.invite.to_dict() + + assert d.get( "object_id" ) == self.object_id + assert datetime.now( tz = utc ) - d.get( "revision" ) < self.delta + assert d.get( "from_user_id" ) == self.from_user_id + assert d.get( "notebook_id" ) == self.notebook_id + assert d.get( "read_write" ) == self.read_write + assert d.get( "owner" ) == self.owner + assert d.get( "redeemed_user_id" ) == None diff --git a/static/js/Wiki.js b/static/js/Wiki.js index 8c5d292..aa0cd1a 100644 --- a/static/js/Wiki.js +++ b/static/js/Wiki.js @@ -12,6 +12,7 @@ function Wiki( invoker ) { this.invoker = invoker; this.rate_plan = evalJSON( getElement( "rate_plan" ).value ); this.storage_usage_high = false; + this.invites = evalJSON( getElement( "invites" ).value ); var total_notes_count_node = getElement( "total_notes_count" ); if ( total_notes_count_node ) @@ -1296,6 +1297,41 @@ Wiki.prototype.share_notebook = function () { ); } + if ( this.invites ) { + var collaborators = createDOM( "ul", { "id": "collaborators" } ); + var viewers = createDOM( "ul", { "id": "viewers" } ); + var owners = createDOM( "ul", { "id": "owners" } ); + + for ( var i in this.invites ) { + var invite = this.invites[ i ]; + if ( invite.owner ) { + appendChildNodes( owners, createDOM( "li", {}, invite.email_address ) ); + } else { + if ( invite.read_write ) + appendChildNodes( collaborators, createDOM( "li", {}, invite.email_address ) ); + else + appendChildNodes( viewers, createDOM( "li", {}, invite.email_address ) ); + } + } + + var invite_area = createDOM( "p", { "id": "invite_area" } ); + + if ( collaborators.childNodes.length > 0 ) { + appendChildNodes( invite_area, createDOM( "h3", {}, "collaborators" ) ); + appendChildNodes( invite_area, collaborators ); + } + if ( viewers.childNodes.length > 0 ) { + appendChildNodes( invite_area, createDOM( "h3", {}, "viewers" ) ); + appendChildNodes( invite_area, viewers ); + } + if ( owners.childNodes.length > 0 ) { + appendChildNodes( invite_area, createDOM( "h3", {}, "owners" ) ); + appendChildNodes( invite_area, owners ); + } + } else { + var invite_area = createDOM( "p", {}, "There are no invites." ); + } + var div = createDOM( "div", {}, createDOM( "form", { "id": "invite_form" }, createDOM( "input", { "type": "hidden", "name": "notebook_id", "value": this.notebook_id } ), @@ -1312,7 +1348,8 @@ Wiki.prototype.share_notebook = function () { createDOM( "input", { "type": "submit", "name": "invite_button", "id": "invite_button", "class": "button", "value": "send invites" } ) - ) + ), + invite_area ) ); diff --git a/static/js/test/Test_wiki.html b/static/js/test/Test_wiki.html index 2f6519c..4c2cafd 100644 --- a/static/js/test/Test_wiki.html +++ b/static/js/test/Test_wiki.html @@ -85,6 +85,7 @@ function test_Wiki() { +
diff --git a/view/Main_page.py b/view/Main_page.py index 5a20396..559047b 100644 --- a/view/Main_page.py +++ b/view/Main_page.py @@ -29,6 +29,7 @@ class Main_page( Page ): conversion = None, rename = False, deleted_id = None, + invites = None, ): startup_note_ids = [ startup_note.object_id for startup_note in startup_notes ] @@ -98,6 +99,7 @@ class Main_page( Page ): Input( type = u"hidden", name = u"note_read_write", id = u"note_read_write", value = json( note_read_write ) ), 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 ) ), Div( id = u"status_area", ),