diff --git a/NEWS b/NEWS index 5658225..2dcd29c 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,7 @@ 1.1.0: January ??, 2007 * Ability to invite people to your notebook as a collaborator or owner. + * Feature to preview a notebook as a viewer would see it. + * Note revisions list now include username of the user who made that revision. * Fixed bug where passwords with special characters broke password hashing. * Fixed bug where link info box summaries sometimes contained HTML tags. diff --git a/controller/Notebooks.py b/controller/Notebooks.py index f63cd30..fbf0958 100644 --- a/controller/Notebooks.py +++ b/controller/Notebooks.py @@ -61,10 +61,11 @@ class Notebooks( object ): revision = Valid_revision(), rename = Valid_bool(), deleted_id = Valid_id(), + preview = Valid_string(), user_id = Valid_id( none_okay = True ), ) def default( self, notebook_id, note_id = None, parent_id = None, revision = None, rename = False, - deleted_id = None, user_id = None ): + deleted_id = None, preview = None, user_id = None ): """ Provide the information necessary to display the page for a particular notebook. If a particular note id is given without a revision, then the most recent version of that note is @@ -82,13 +83,39 @@ class Notebooks( object ): @param rename: whether this is a new notebook and should be renamed (optional, defaults to False) @type deleted_id: unicode or NoneType @param deleted_id: id of the notebook that was just deleted, if any (optional) + @type preview: unicode + @param preview: type of access with which to preview this notebook, either "collaborator", + "viewer", "owner", or "default" (optional, defaults to "default"). access must + be equal to or lower than user's own access level to this notebook @type user_id: unicode or NoneType @param user_id: id of current logged-in user (if any) @rtype: unicode @return: rendered HTML page """ result = self.__users.current( user_id ) - result.update( self.contents( notebook_id, note_id, revision, user_id ) ) + + if preview == u"collaborator": + read_write = True + owner = False + result[ u"notebooks" ] = [ + notebook for notebook in result[ "notebooks" ] if notebook.object_id == notebook_id + ] + result[ u"notebooks" ][ 0 ].owner = False + elif preview == u"viewer": + read_write = False + owner = False + result[ u"notebooks" ] = [ + notebook for notebook in result[ "notebooks" ] if notebook.object_id == notebook_id + ] + result[ u"notebooks" ][ 0 ].read_write = False + result[ u"notebooks" ][ 0 ].owner = False + elif preview in ( u"owner", u"default", None ): + read_write = True + owner = True + else: + raise Access_error() + + result.update( self.contents( notebook_id, note_id, revision, read_write, owner, user_id ) ) result[ "parent_id" ] = parent_id if revision: result[ "note_read_write" ] = False @@ -96,7 +123,7 @@ class Notebooks( object ): # if the user doesn't have any storage bytes yet, they're a new user, so see what type of # conversion this is (demo or signup) if result[ "user" ].storage_bytes == 0: - if u"demo" in [ note.title for note in result[ "startup_notes" ] ]: + if u"this is a demo" in [ note.title for note in result[ "startup_notes" ] ]: result[ "conversion" ] = u"demo" else: result[ "conversion" ] = u"signup" @@ -105,7 +132,7 @@ class Notebooks( object ): return result - def contents( self, notebook_id, note_id = None, revision = None, user_id = None ): + def contents( self, notebook_id, note_id = None, revision = None, read_write = True, owner = True, user_id = None ): """ Return the startup notes for the given notebook. Optionally include a single requested note as well. @@ -116,6 +143,10 @@ class Notebooks( object ): @param note_id: id of single note in this notebook to return (optional) @type revision: unicode or NoneType @param revision: revision timestamp of the provided note (optional) + @type read_write: bool or NoneType + @param read_write: whether the notebook should be returned as read-write (optional, defaults to True) + @type owner: bool or NoneType + @param owner: whether the notebook should be returned as owner-level access (optional, defaults to True) @type user_id: unicode or NoneType @param user_id: id of current logged-in user (if any) @rtype: dict @@ -136,10 +167,14 @@ class Notebooks( object ): if notebook is None: raise Access_error() - if not self.__users.check_access( user_id, notebook_id, read_write = True ): + if read_write is False: + notebook.read_write = False + elif not self.__users.check_access( user_id, notebook_id, read_write = True ): notebook.read_write = False - if not self.__users.check_access( user_id, notebook_id, owner = True ): + if owner is False: + notebook.owner = False + elif not self.__users.check_access( user_id, notebook_id, owner = True ): notebook.owner = False if note_id: diff --git a/controller/test/Test_notebooks.py b/controller/test/Test_notebooks.py index 7d60765..a16e69a 100644 --- a/controller/test/Test_notebooks.py +++ b/controller/test/Test_notebooks.py @@ -97,10 +97,15 @@ class Test_notebooks( Test_controller ): assert result.get( u"user" ).object_id == self.user.object_id assert len( result.get( u"notebooks" ) ) == 3 + assert result.get( u"notebooks" )[ 0 ].object_id == self.notebook.object_id + assert result.get( u"notebooks" )[ 0 ].read_write == True + assert result.get( u"notebooks" )[ 0 ].owner == True assert result.get( u"login_url" ) is None assert result.get( u"logout_url" ) assert result.get( u"rate_plan" ) assert result.get( u"notebook" ).object_id == self.notebook.object_id + assert result.get( u"notebook" ).read_write == True + assert result.get( u"notebook" ).owner == True assert len( result.get( u"startup_notes" ) ) == 1 assert result[ "total_notes_count" ] == 2 assert result.get( u"notes" ) == [] @@ -115,6 +120,194 @@ class Test_notebooks( Test_controller ): user = self.database.load( User, self.user.object_id ) assert user.storage_bytes == 0 + def test_default_as_preview_viewer( self ): + self.login() + + result = self.http_get( + "/notebooks/%s?preview=viewer" % self.notebook.object_id, + session_id = self.session_id, + ) + + assert result.get( u"user" ).object_id == self.user.object_id + assert len( result.get( u"notebooks" ) ) == 1 + assert result.get( u"notebooks" )[ 0 ].object_id == self.notebook.object_id + assert result.get( u"notebooks" )[ 0 ].read_write == False + assert result.get( u"notebooks" )[ 0 ].owner == False + assert result.get( u"login_url" ) is None + assert result.get( u"logout_url" ) + assert result.get( u"rate_plan" ) + assert result.get( u"notebook" ).object_id == self.notebook.object_id + assert result.get( u"notebook" ).read_write == False + assert result.get( u"notebook" ).owner == False + assert len( result.get( u"startup_notes" ) ) == 1 + assert result[ "total_notes_count" ] == 2 + assert result.get( u"notes" ) == [] + 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 + + def test_default_as_preview_collaborator( self ): + self.login() + + result = self.http_get( + "/notebooks/%s?preview=collaborator" % self.notebook.object_id, + session_id = self.session_id, + ) + + assert result.get( u"user" ).object_id == self.user.object_id + assert len( result.get( u"notebooks" ) ) == 1 + assert result.get( u"notebooks" )[ 0 ].object_id == self.notebook.object_id + assert result.get( u"notebooks" )[ 0 ].read_write == True + assert result.get( u"notebooks" )[ 0 ].owner == False + assert result.get( u"login_url" ) is None + assert result.get( u"logout_url" ) + assert result.get( u"rate_plan" ) + assert result.get( u"notebook" ).object_id == self.notebook.object_id + assert result.get( u"notebook" ).read_write == True + assert result.get( u"notebook" ).owner == False + assert len( result.get( u"startup_notes" ) ) == 1 + assert result[ "total_notes_count" ] == 2 + assert result.get( u"notes" ) == [] + 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 + + def test_default_as_preview_owner( self ): + self.login() + + result = self.http_get( + "/notebooks/%s?preview=owner" % self.notebook.object_id, + session_id = self.session_id, + ) + + assert result.get( u"user" ).object_id == self.user.object_id + assert len( result.get( u"notebooks" ) ) == 3 + assert result.get( u"notebooks" )[ 0 ].object_id == self.notebook.object_id + assert result.get( u"notebooks" )[ 0 ].read_write == True + assert result.get( u"notebooks" )[ 0 ].owner == True + assert result.get( u"login_url" ) is None + assert result.get( u"logout_url" ) + assert result.get( u"rate_plan" ) + assert result.get( u"notebook" ).object_id == self.notebook.object_id + assert result.get( u"notebook" ).read_write == True + assert result.get( u"notebook" ).owner == True + assert len( result.get( u"startup_notes" ) ) == 1 + assert result[ "total_notes_count" ] == 2 + assert result.get( u"notes" ) == [] + 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 + + def test_default_as_preview_viewer_with_viewer_access( self ): + self.login() + + result = self.http_get( + "/notebooks/%s?preview=viewer" % self.anon_notebook.object_id, + session_id = self.session_id, + ) + + assert result.get( u"user" ).object_id == self.user.object_id + assert len( result.get( u"notebooks" ) ) == 1 + assert result.get( u"notebooks" )[ 0 ].object_id == self.anon_notebook.object_id + assert result.get( u"notebooks" )[ 0 ].read_write == False + assert result.get( u"notebooks" )[ 0 ].owner == False + assert result.get( u"login_url" ) is None + assert result.get( u"logout_url" ) + assert result.get( u"rate_plan" ) + assert result.get( u"notebook" ).object_id == self.anon_notebook.object_id + assert result.get( u"notebook" ).read_write == False + assert result.get( u"notebook" ).owner == False + assert len( result.get( u"startup_notes" ) ) == 0 + assert result[ "total_notes_count" ] == 0 + assert result.get( u"notes" ) == [] + assert result.get( u"parent_id" ) == None + assert result.get( u"note_read_write" ) in ( None, True ) + assert len( result[ "invites" ] ) == 0 + + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes == 0 + + def test_default_as_preview_collaborator_with_viewer_access( self ): + self.login() + + result = self.http_get( + "/notebooks/%s?preview=collaborator" % self.anon_notebook.object_id, + session_id = self.session_id, + ) + + # even though a collaborator preview is being requested, this user only has preview-level + # access. so read_write should be False on the returned notebook + assert result.get( u"user" ).object_id == self.user.object_id + assert len( result.get( u"notebooks" ) ) == 1 + assert result.get( u"notebooks" )[ 0 ].object_id == self.anon_notebook.object_id + assert result.get( u"notebooks" )[ 0 ].read_write == False + assert result.get( u"notebooks" )[ 0 ].owner == False + assert result.get( u"login_url" ) is None + assert result.get( u"logout_url" ) + assert result.get( u"rate_plan" ) + assert result.get( u"notebook" ).object_id == self.anon_notebook.object_id + assert result.get( u"notebook" ).read_write == False + assert result.get( u"notebook" ).owner == False + assert len( result.get( u"startup_notes" ) ) == 0 + assert result[ "total_notes_count" ] == 0 + assert result.get( u"notes" ) == [] + assert result.get( u"parent_id" ) == None + assert result.get( u"note_read_write" ) in ( None, True ) + assert len( result[ "invites" ] ) == 0 + + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes == 0 + + def test_default_as_preview_owner_with_viewer_access( self ): + self.login() + + result = self.http_get( + "/notebooks/%s?preview=owner" % self.anon_notebook.object_id, + session_id = self.session_id, + ) + + assert result.get( u"user" ).object_id == self.user.object_id + assert len( result.get( u"notebooks" ) ) == 3 + assert result.get( u"notebooks" )[ 2 ].object_id == self.anon_notebook.object_id + assert result.get( u"notebooks" )[ 2 ].read_write == False + assert result.get( u"notebooks" )[ 2 ].owner == False + assert result.get( u"login_url" ) is None + assert result.get( u"logout_url" ) + assert result.get( u"rate_plan" ) + assert result.get( u"notebook" ).object_id == self.anon_notebook.object_id + assert result.get( u"notebook" ).read_write == False + assert result.get( u"notebook" ).owner == False + assert len( result.get( u"startup_notes" ) ) == 0 + assert result[ "total_notes_count" ] == 0 + assert result.get( u"notes" ) == [] + assert result.get( u"parent_id" ) == None + assert result.get( u"note_read_write" ) in ( None, True ) + assert len( result[ "invites" ] ) == 0 + + user = self.database.load( User, self.user.object_id ) + assert user.storage_bytes == 0 + def test_default_with_note( self ): self.login() @@ -125,10 +318,15 @@ class Test_notebooks( Test_controller ): assert result.get( u"user" ).object_id == self.user.object_id assert len( result.get( u"notebooks" ) ) == 3 + assert result.get( u"notebooks" )[ 0 ].object_id == self.notebook.object_id + assert result.get( u"notebooks" )[ 0 ].read_write == True + assert result.get( u"notebooks" )[ 0 ].owner == True assert result.get( u"login_url" ) is None assert result.get( u"logout_url" ) assert result.get( u"rate_plan" ) assert result.get( u"notebook" ).object_id == self.notebook.object_id + assert result.get( u"notebook" ).read_write == True + assert result.get( u"notebook" ).owner == True assert len( result.get( u"startup_notes" ) ) == 1 assert result[ "total_notes_count" ] == 2 @@ -160,10 +358,15 @@ class Test_notebooks( Test_controller ): assert result.get( u"user" ).object_id == self.user.object_id assert len( result.get( u"notebooks" ) ) == 3 + assert result.get( u"notebooks" )[ 0 ].object_id == self.notebook.object_id + assert result.get( u"notebooks" )[ 0 ].read_write == True + assert result.get( u"notebooks" )[ 0 ].owner == True assert result.get( u"login_url" ) is None assert result.get( u"logout_url" ) assert result.get( u"rate_plan" ) assert result.get( u"notebook" ).object_id == self.notebook.object_id + assert result.get( u"notebook" ).read_write == True + assert result.get( u"notebook" ).owner == True assert len( result.get( u"startup_notes" ) ) == 1 assert result[ "total_notes_count" ] == 2 @@ -193,10 +396,15 @@ class Test_notebooks( Test_controller ): assert result.get( u"user" ).object_id == self.user.object_id assert len( result.get( u"notebooks" ) ) == 3 + assert result.get( u"notebooks" )[ 0 ].object_id == self.notebook.object_id + assert result.get( u"notebooks" )[ 0 ].read_write == True + assert result.get( u"notebooks" )[ 0 ].owner == True assert result.get( u"login_url" ) is None assert result.get( u"logout_url" ) assert result.get( u"rate_plan" ) assert result.get( u"notebook" ).object_id == self.notebook.object_id + assert result.get( u"notebook" ).read_write == True + assert result.get( u"notebook" ).owner == True assert len( result.get( u"startup_notes" ) ) == 1 assert result[ "total_notes_count" ] == 2 assert result.get( u"notes" ) == [] @@ -235,6 +443,56 @@ class Test_notebooks( Test_controller ): user = self.database.load( User, self.user.object_id ) assert user.storage_bytes == 0 + def test_contents_with_read_write_false( self ): + result = cherrypy.root.notebooks.contents( + notebook_id = self.notebook.object_id, + read_write = False, + 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 ) == 1 + invite = invites[ 0 ] + assert invite.object_id == self.invite.object_id + + assert notebook.object_id == self.notebook.object_id + assert notebook.read_write == False + 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_owner_false( self ): + result = cherrypy.root.notebooks.contents( + notebook_id = self.notebook.object_id, + owner = False, + 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 ) == 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 == False + 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_note( self ): result = cherrypy.root.notebooks.contents( notebook_id = self.notebook.object_id, diff --git a/static/js/Wiki.js b/static/js/Wiki.js index ed153fb..ed5e22b 100644 --- a/static/js/Wiki.js +++ b/static/js/Wiki.js @@ -1330,7 +1330,19 @@ Wiki.prototype.share_notebook = function () { ) ), invite_area - ) + ), + createDOM( "div", {}, + createDOM( + "a", { "href": "/notebooks/" + this.notebook_id + "?preview=viewer", "target": "_new" }, + "Preview this notebook as a viewer." + ) + ), + this.rate_plan.notebook_collaboration ? createDOM( "div", {}, + createDOM( + "a", { "href": "/notebooks/" + this.notebook_id + "?preview=collaborator", "target": "_new" }, + "Preview this notebook as a collaborator." + ) + ) : null ); this.create_editor( "share_notebook", "

share this notebook

" + div.innerHTML, undefined, undefined, undefined, false, true, true, getElement( "notes_top" ) );