def __iter__(self): for item in self.previous: keys = item.keys() typekey = self.typekey(*keys)[0] if item[typekey] != 'plone.Comment': # not a comment yield item; continue pathkey = self.pathkey(*item.keys())[0] if not pathkey: # not enough info yield item; continue path = item[pathkey] ob = self.context.unrestrictedTraverse(path.lstrip('/'), None) if ob is None: yield item; continue # object not found # XXX make sure comment doesn't exist already? conversation = IConversation(ob) comment = createObject('plone.Comment') comment.text = item['text'] comment.author_name = item['author_name'] comment.author_email = item['author_email'] comment.creation_date = DateTime(item['created']).asdatetime() comment.modification_date = comment.creation_date in_reply_to = item.get('_in_reply_to', 0) if in_reply_to: comment.in_reply_to = self.comment_map[in_reply_to] id = conversation.addComment(comment) self.comment_map[item['_comment_id']] = id yield item
def setUp(self): self.portal = self.layer['portal'] setRoles(self.portal, TEST_USER_ID, ['Manager']) # Create a conversation. conversation = IConversation(self.portal.doc1) comment1 = createObject('plone.Comment') comment1.text = 'Comment Text' comment1.creator = "jim" comment1.author_username = "******" comment1.creation_date = datetime(2006, 9, 17, 14, 18, 12) comment1.modification_date = datetime(2006, 9, 17, 14, 18, 12) self.new_id1 = conversation.addComment(comment1) comment2 = createObject('plone.Comment') comment2.text = 'Comment Text' comment2.creator = "emma" comment2.author_username = "******" comment2.creation_date = datetime(2007, 12, 13, 4, 18, 12) comment2.modification_date = datetime(2007, 12, 13, 4, 18, 12) self.new_id2 = conversation.addComment(comment2) comment3 = createObject('plone.Comment') comment3.text = 'Comment Text' comment3.creator = "lukas" comment3.author_username = "******" comment3.creation_date = datetime(2009, 4, 12, 11, 12, 12) comment3.modification_date = datetime(2009, 4, 12, 11, 12, 12) self.new_id3 = conversation.addComment(comment3) self.conversation = conversation
def test_add_and_remove_interface_on_catalog(self): self._add_comment('one alert') # acquisition wrapped version of the comment comment = IConversation(self.document).values()[0] condition = TextAlertCondition() condition.stop_words = u'one alert\nanother alert' # adds the marker interface executable = getMultiAdapter( (self.portal, condition, CommentDummyEvent(comment)), IExecutable ) executable() brains = api.content.find( self.portal, object_provides=IHasStopWords.__identifier__ ) self.assertEqual(len(brains), 1) comment.text = 'no longer creating an alert' executable() brains = api.content.find( self.portal, object_provides=IHasStopWords.__identifier__ ) self.assertEqual(len(brains), 0)
def setUp(self): self.portal = self.layer["portal"] setRoles(self.portal, TEST_USER_ID, ["Manager"]) self.portal.invokeFactory(id="doc1", Title="Document 1", type_name="Document") self.catalog = getToolByName(self.portal, "portal_catalog") conversation = IConversation(self.portal.doc1) comment1 = createObject("plone.Comment") comment1.title = "Comment 1" comment1.text = "Comment text" comment1.creator = "Jim" comment1.author_username = "******" comment1.creation_date = datetime(2006, 9, 17, 14, 18, 12) comment1.modification_date = datetime(2006, 9, 17, 14, 18, 12) new_comment1_id = conversation.addComment(comment1) self.comment_id = new_comment1_id brains = self.catalog.searchResults( dict(path={"query": "/".join(self.portal.doc1.getPhysicalPath())}, portal_type="Document") ) self.conversation = conversation self.brains = brains self.doc1_brain = brains[0] self.comment1 = comment1 self.new_comment1_id = new_comment1_id
def test_delete_comment(self): # Add and remove a comment to a CommentReplies adapter # Create a conversation. In this case we doesn't assign it to an # object, as we just want to check the Conversation object API. conversation = IConversation(self.portal.doc1) # Add a comment to the conversation replies = IReplies(conversation) comment = createObject('plone.Comment') comment.text = 'Comment text' new_id = replies.addComment(comment) comment = self.portal.doc1.restrictedTraverse( '++conversation++default/{0}'.format(new_id) ) # Add a reply to the CommentReplies adapter of the first comment re_comment = createObject('plone.Comment') re_comment.text = 'Comment text' replies = IReplies(comment) new_re_id = replies.addComment(re_comment) # Remove the reply to the CommentReplies adapter del replies[new_re_id] # Make sure there is no comment left in CommentReplies self.assertEqual(len(replies), 0) # Make sure the first comment is still in the conversation self.assertEqual(conversation.total_comments(), 1)
def test_get_commenter_portrait(self): # Add a user with a member image self.membershipTool.addMember('jim', 'Jim', ['Member'], []) self.memberdata._setPortrait(Image( id='jim', file=dummy.File(), title='' ), 'jim') self.assertEqual( self.memberdata._getPortrait('jim').getId(), 'jim' ) self.assertEqual( self.memberdata._getPortrait('jim').meta_type, 'Image' ) # Add a conversation with a comment conversation = IConversation(self.portal.doc1) comment = createObject('plone.Comment') comment.text = 'Comment text' comment.Creator = 'Jim' comment.author_username = '******' conversation.addComment(comment) # Call get_commenter_portrait method of the viewlet self.viewlet.update() portrait_url = self.viewlet.get_commenter_portrait('jim') # Check if the correct member image URL is returned self.assertEqual( portrait_url, 'http://nohost/plone/portal_memberdata/portraits/jim' )
def comments_count(self): context = self.content_context try: conversation = IConversation(context) except Exception: return 0 return conversation.total_comments()
def test_get_commenter_portrait_without_userimage(self): # Create a user without a user image self.membershipTool.addMember('jim', 'Jim', ['Member'], []) # Add a conversation with a comment conversation = IConversation(self.portal.doc1) comment = createObject('plone.Comment') comment.text = 'Comment text' comment.Creator = 'Jim' comment.author_username = '******' conversation.addComment(comment) # Call get_commenter_portrait method of the viewlet self.viewlet.update() portrait_url = self.viewlet.get_commenter_portrait('jim') # Check if the correct default member image URL is returned. # Note that Products.PlonePAS 4.0.5 and later have .png and # earlier versions have .gif. self.assertTrue( portrait_url in ( 'http://nohost/plone/defaultUser.png', 'http://nohost/plone/defaultUser.gif' ) )
def setUp(self): self.portal = self.layer['portal'] setRoles(self.portal, TEST_USER_ID, ['Manager']) self.portal.invokeFactory(id='doc1', title='Document 1', type_name='Document') self.catalog = getToolByName(self.portal, 'portal_catalog') conversation = IConversation(self.portal.doc1) self.conversation = conversation comment1 = createObject('plone.Comment') comment1.text = 'Comment text' comment1.creator = 'jim' comment1.author_name = 'Jim' new_comment1_id = conversation.addComment(comment1) self.comment_id = new_comment1_id # Comment brain self.comment = self.portal.doc1.restrictedTraverse( '++conversation++default/%s' % new_comment1_id) brains = self.catalog.searchResults(dict( path={ 'query': '/'.join(self.comment.getPhysicalPath()) } )) self.comment_brain = brains[0]
def setUp(self): self.portal = self.layer['portal'] setRoles(self.portal, TEST_USER_ID, ['Manager']) self.portal.invokeFactory(id='doc1', title='Document 1', type_name='Document') # Create a conversation. In this case we doesn't assign it to an # object, as we just want to check the Conversation object API. conversation = IConversation(self.portal.doc1) # Add a comment. Note: in real life, we always create comments via the # factory to allow different factories to be swapped in comment = createObject('plone.Comment') comment.text = 'Lorem ipsum dolor sit amet.' comment.creator = "jim" comment.author_name = "Jim" comment.creation_date = datetime(2006, 9, 17, 14, 18, 12) comment.modification_date = datetime(2008, 3, 12, 7, 32, 52) self.comment_id = conversation.addComment(comment) self.comment = comment.__of__(conversation) self.conversation = conversation
def test_comment_uid_differs_from_content_uid(self): conversation = IConversation(self.portal.doc1) comment1 = createObject('plone.Comment') conversation.addComment(comment1) comment_brain = self.catalog.searchResults( portal_type = 'Discussion Item')[0] self.assertNotEqual(self.document_brain.UID, comment_brain.UID)
def test_get_replies_with_workflow_actions(self): self.assertFalse(self.viewlet.get_replies(workflow_actions=True)) comment = createObject('plone.Comment') comment.text = 'Comment text' conversation = IConversation(self.portal.doc1) c1 = conversation.addComment(comment) self.assertEqual( len(tuple(self.viewlet.get_replies(workflow_actions=True))), 1 ) # Enable moderation workflow self.workflowTool.setChainForPortalTypes( ('Discussion Item',), ('comment_review_workflow,') ) # Check if workflow actions are available reply = self.viewlet.get_replies(workflow_actions=True).next() self.assertTrue('actions' in reply) self.assertEqual( reply['actions'][0]['id'], 'publish' ) self.assertEqual( reply['actions'][0]['url'], 'http://nohost/plone/doc1/++conversation++default/%s' % int(c1) + '/content_status_modify?workflow_action=publish' )
def test_uid(self): conversation = IConversation(self.portal.doc1) comment1 = createObject('plone.Comment') conversation.addComment(comment1) comment_brain = self.catalog.searchResults( portal_type = 'Discussion Item')[0] self.assertTrue(comment_brain.UID)
def findObjects(origin): """ generator to recursively find and yield all zope objects below the given start point """ traverse = origin.unrestrictedTraverse base = '/'.join(origin.getPhysicalPath()) cut = len(base) + 1 paths = [base] for idx, path in enumerate(paths): obj = traverse(path) yield path[cut:], obj if hasattr(aq_base(obj), 'objectIds'): from zope.component.interfaces import ComponentLookupError try: for id in obj.objectIds(): paths.insert(idx + 1, path + '/' + id) except ComponentLookupError: logger.error( 'Can not list sub-objects of object {0}'.format(path) ) try: conversation = IConversation(obj) except TypeError: continue for comment in conversation.getComments(): comment_path = '/'.join(comment.getPhysicalPath()[-2:]) paths.insert(idx + 1, path + '/' + comment_path)
def test_workflow(self): """Basic test for the 'comment_review_workflow' """ self.portal.portal_workflow.setChainForPortalTypes( ('Discussion Item',), ('comment_review_workflow,')) conversation = IConversation(self.portal.doc1) comment1 = createObject('plone.Comment') new_comment1_id = conversation.addComment(comment1) comment = conversation[new_comment1_id] # Make sure comments use the 'comment_review_workflow' chain = self.portal.portal_workflow.getChainFor(comment) self.assertEqual(('comment_review_workflow',), chain) # Ensure the initial state was entered and recorded self.assertEqual( 1, len(comment.workflow_history['comment_review_workflow']) ) self.assertEqual( None, comment.workflow_history['comment_review_workflow'][0]['action'] ) self.assertEqual( 'pending', self.portal.portal_workflow.getInfoFor(comment, 'review_state') )
def setUp(self): self.portal = self.layer['portal'] setRoles(self.portal, TEST_USER_ID, ['Manager']) workflow = self.portal.portal_workflow workflow.doActionFor(self.portal.doc1, 'publish') # Create a conversation. conversation = IConversation(self.portal.doc1) comment1 = createObject('plone.Comment') comment1.text = 'Comment Text' comment1.creator = 'jim' comment1.author_username = '******' comment1.creation_date = datetime(2006, 9, 17, 14, 18, 12) comment1.modification_date = datetime(2006, 9, 17, 14, 18, 12) self.new_id1 = conversation.addComment(comment1) comment2 = createObject('plone.Comment') comment2.text = 'Comment Text' comment2.creator = 'emma' comment2.author_username = '******' comment2.creation_date = datetime(2007, 12, 13, 4, 18, 12) comment2.modification_date = datetime(2007, 12, 13, 4, 18, 12) self.new_id2 = conversation.addComment(comment2) comment3 = createObject('plone.Comment') comment3.text = 'Comment Text' comment3.creator = 'lukas' comment3.author_username = '******' comment3.creation_date = datetime(2009, 4, 12, 11, 12, 12) comment3.modification_date = datetime(2009, 4, 12, 11, 12, 12) self.new_id3 = conversation.addComment(comment3) self.conversation = conversation
def setUp(self): # Setup session manager ztc.utils.setupCoreSessions(self.layer['app']) # Setup sandbox self.portal = self.layer['portal'] self.request = self.layer['request'] # Setup current user properties member = self.portal.portal_membership.getMemberById(TEST_USER_ID) member.setMemberProperties({ 'fullname': 'X Manager', 'email': '*****@*****.**' }) setRoles(self.portal, TEST_USER_ID, ['Manager']) name = self.portal.invokeFactory( id='doc1', title='Document 1', type_name='Document') self.document = self.portal[name] comment = createObject('plone.Comment') comment.text = "This is a comment" comment.author_username = "******" comment.author_name = "Jim" comment.author_email = "*****@*****.**" conversation = IConversation(self.document) conversation.addComment(comment)
def test_traversal(self): # Create a nested structure of comment replies and check the traversal # make sure comments are traversable, have an id, absolute_url and # physical path conversation = IConversation(self.portal.doc1) comment1 = createObject('plone.Comment') comment1.text = 'Comment text' conversation.addComment(comment1) comment = createObject('plone.Comment') comment.text = 'Comment text' new_id = conversation.addComment(comment) comment = self.portal.doc1.restrictedTraverse( '++conversation++default/%s' % new_id) # Add a reply to the CommentReplies adapter of the first comment re_comment = createObject('plone.Comment') re_comment.text = 'Comment text' replies = IReplies(comment) new_re_id = replies.addComment(re_comment) re_comment = self.portal.doc1.restrictedTraverse( '++conversation++default/%s' % new_re_id) # Add a reply to the reply re_re_comment = createObject('plone.Comment') re_re_comment.text = 'Comment text' replies = IReplies(re_comment) new_re_re_id = replies.addComment(re_re_comment) re_re_comment = self.portal.doc1.restrictedTraverse( '++conversation++default/%s' % new_re_re_id) # Add a reply to the replies reply re_re_re_comment = createObject('plone.Comment') re_re_re_comment.text = 'Comment text' replies = IReplies(re_re_comment) new_re_re_re_id = replies.addComment(re_re_re_comment) re_re_re_comment = self.portal.doc1.restrictedTraverse( '++conversation++default/%s' % new_re_re_re_id) self.assertEqual(('', 'plone', 'doc1', '++conversation++default', str(new_id)), comment.getPhysicalPath()) self.assertEqual('http://nohost/plone/doc1/++conversation++default/' + str(new_id), comment.absolute_url()) self.assertEqual(('', 'plone', 'doc1', '++conversation++default', str(new_re_id)), re_comment.getPhysicalPath()) self.assertEqual('http://nohost/plone/doc1/++conversation++default/' + str(new_re_id), re_comment.absolute_url()) self.assertEqual(('', 'plone', 'doc1', '++conversation++default', str(new_re_re_id)), re_re_comment.getPhysicalPath()) self.assertEqual('http://nohost/plone/doc1/++conversation++default/' + str(new_re_re_id), re_re_comment.absolute_url()) self.assertEqual(('', 'plone', 'doc1', '++conversation++default', str(new_re_re_re_id)), re_re_re_comment.getPhysicalPath()) self.assertEqual('http://nohost/plone/doc1/++conversation++default/' + str(new_re_re_re_id), re_re_re_comment.absolute_url())
def test_has_replies(self): self.assertEqual(self.viewlet.has_replies(), False) comment = createObject('plone.Comment') comment.text = 'Comment text' conversation = IConversation(self.portal.doc1) conversation.addComment(comment) self.assertEqual(self.viewlet.has_replies(), True)
def setUp(self): self.portal = self.layer['portal'] setRoles(self.portal, TEST_USER_ID, ['Manager']) self.portal.invokeFactory( id='doc1', Title='Document 1', type_name='Document' ) self.catalog = getToolByName(self.portal, 'portal_catalog') conversation = IConversation(self.portal.doc1) comment1 = createObject('plone.Comment') comment1.title = 'Comment 1' comment1.text = 'Comment text' comment1.creator = 'jim' comment1.author_username = '******' comment1.creation_date = datetime(2006, 9, 17, 14, 18, 12) comment1.modification_date = datetime(2006, 9, 17, 14, 18, 12) new_comment1_id = conversation.addComment(comment1) self.comment_id = new_comment1_id brains = self.catalog.searchResults(dict( path={ 'query': '/'.join(self.portal.doc1.getPhysicalPath()) }, portal_type="Document" )) self.conversation = conversation self.brains = brains self.doc1_brain = brains[0] self.comment1 = comment1 self.new_comment1_id = new_comment1_id
def test_view(self): # make sure that the comment view is there and redirects to the right # URL # Create a conversation. In this case we doesn't assign it to an # object, as we just want to check the Conversation object API. conversation = IConversation(self.portal.doc1) # Create a comment comment1 = createObject('plone.Comment') comment1.text = 'Comment text' # Add comment to the conversation new_comment1_id = conversation.addComment(comment1) comment = self.portal.doc1.restrictedTraverse( '++conversation++default/{0}'.format(new_comment1_id) ) # make sure the view is there self.assertTrue(getMultiAdapter((comment, self.request), name='view')) # make sure the HTTP redirect (status code 302) works when a comment # is called directly view = View(comment, self.request) View.__call__(view) self.assertEqual(self.request.response.status, 302)
def setUp(self): self.portal = self.layer['portal'] setRoles(self.portal, TEST_USER_ID, ['Manager']) self.portal.invokeFactory('Folder', 'test-folder') self.folder = self.portal['test-folder'] # Allow discussion on the Document content type self.portal.portal_types['Document'].allow_discussion = True # Set workflow for Discussion item to review workflow self.portal.portal_workflow.setChainForPortalTypes( ('Discussion Item',), ('comment_review_workflow',)) # Create a Document self.portal.invokeFactory('Document', 'doc1') self.portal_discussion = self.portal.portal_discussion # Create a conversation for this Document conversation = IConversation(self.portal.doc1) # Add a comment. comment = createObject('plone.Comment') comment.text = 'Comment text' comment_id = conversation.addComment(comment) comment = self.portal.doc1.restrictedTraverse( '++conversation++default/%s' % comment_id) self.conversation = conversation self.comment_id = comment_id self.comment = comment setRoles(self.portal, TEST_USER_ID, ['Reviewer']) alsoProvides(self.portal.REQUEST, IDiscussionLayer)
def notify_content_object_moved(obj, event): """Update all comments of a content object that has been moved. """ if event.oldParent is None or event.newParent is None \ or event.oldName is None or event.newName is None: return # This method is also called for sublocations of moved objects. We # therefore can't assume that event.object == obj and event. # {old,new}{Parent,Name} may refer to the actually moved object further up # in the object hierarchy. The object is already moved at this point. so # obj.getPhysicalPath retruns the new path get the part of the path that # was moved. moved_path = obj.getPhysicalPath()[ len(event.newParent.getPhysicalPath()) + 1: ] # Remove comments at the old location from catalog catalog = getToolByName(obj, 'portal_catalog') old_path = '/'.join( event.oldParent.getPhysicalPath() + (event.oldName,) + moved_path ) brains = catalog.searchResults(dict( path={'query': old_path}, portal_type="Discussion Item" )) for brain in brains: catalog.uncatalog_object(brain.getPath()) # Reindex comment at the new location conversation = IConversation(obj, None) if conversation is not None: for comment in conversation.getComments(): comment.reindexObject()
def test_add_comment(self): # Create a conversation. In this case we doesn't assign it to an # object, as we just want to check the Conversation object API. conversation = IConversation(self.portal.doc1) # Add a comment. Note: in real life, we always create comments via the # factory to allow different factories to be swapped in comment = createObject('plone.Comment') comment.text = 'Comment text' new_id = conversation.addComment(comment) # Check that the conversation methods return the correct data self.assertTrue(isinstance(comment.comment_id, long)) self.assertTrue(IComment.providedBy(conversation[new_id])) self.assertEqual( aq_base(conversation[new_id].__parent__), aq_base(conversation) ) self.assertEqual(new_id, comment.comment_id) self.assertEqual(len(list(conversation.getComments())), 1) self.assertEqual(len(tuple(conversation.getThreads())), 1) self.assertEqual(conversation.total_comments(), 1) self.assertTrue( conversation.last_comment_date - datetime.utcnow() < timedelta(seconds=1) )
def setUp(self): self.portal = self.layer['portal'] setRoles(self.portal, TEST_USER_ID, ['Manager']) self.portal.invokeFactory('Folder', 'test-folder') self.folder = self.portal['test-folder'] self.catalog = self.portal.portal_catalog self.workflow = self.portal.portal_workflow self.workflow.setChainForPortalTypes(['Document'], 'one_state_workflow') self.folder.invokeFactory('Document', 'doc1') self.doc = self.folder.doc1 # Add a comment conversation = IConversation(self.folder.doc1) comment = createObject('plone.Comment') comment.text = 'Comment text' cid = conversation.addComment(comment) self.comment = self.folder.doc1.restrictedTraverse(\ '++conversation++default/%s' % cid) self.portal.acl_users._doAddUser('member', 'secret', ['Member'], []) self.portal.acl_users._doAddUser('reviewer', 'secret', ['Reviewer'], []) self.portal.acl_users._doAddUser('manager', 'secret', ['Manager'], []) self.portal.acl_users._doAddUser('editor' , ' secret', ['Editor'],[]) self.portal.acl_users._doAddUser('reader', 'secret', ['Reader'], [])
def test_traversal(self): # make sure comments are traversable, have an id, absolute_url and # physical path conversation = IConversation(self.portal.doc1) comment1 = createObject('plone.Comment') comment1.text = 'Comment text' new_comment1_id = conversation.addComment(comment1) comment = self.portal.doc1.restrictedTraverse( '++conversation++default/{0}'.format(new_comment1_id) ) self.assertTrue(IComment.providedBy(comment)) self.assertEqual( ( '', 'plone', 'doc1', '++conversation++default', str(new_comment1_id) ), comment.getPhysicalPath() ) self.assertEqual( 'http://nohost/plone/doc1/++conversation++default/' + str(new_comment1_id), comment.absolute_url() )
def write_comments(self): comments = self._data['comments'] for com in comments: cid = com['cid'] posts = self._posts try: post = posts[cid] except KeyError: continue conversation = IConversation(post) date = DateTime(com['date']).asdatetime() comment = createObject('plone.Comment') comment.text = fix_text(com['text']) comment.author_name = comment.creator = com['name'] comment.author_email = com['email'] comment.creation_date = comment.modification_date = date conversation.addComment(comment) transaction.commit() print("Wrote comment by %s" % comment.creator)
def test_removedEvent(self): self.assertFalse(self.registry.commentRemoved) comment = createObject('plone.Comment') conversation = IConversation(self.document) cid = conversation.addComment(comment) del conversation[cid] self.assertTrue(self.registry.commentRemoved)
def setUp(self): self.app = self.layer["app"] self.portal = self.layer["portal"] self.request = self.layer["request"] setRoles(self.portal, TEST_USER_ID, ["Manager"]) typetool = self.portal.portal_types typetool.constructContent("Document", self.portal, "doc1") self.wf = getToolByName(self.portal, "portal_workflow", None) self.context = self.portal self.portal.portal_workflow.setChainForPortalTypes(("Discussion Item",), "comment_review_workflow") self.wf_tool = self.portal.portal_workflow # Add a conversation with three comments conversation = IConversation(self.portal.doc1) comment1 = createObject("plone.Comment") comment1.title = "Comment 1" comment1.text = "Comment text" comment1.Creator = "Jim" new_id_1 = conversation.addComment(comment1) self.comment1 = self.portal.doc1.restrictedTraverse("++conversation++default/%s" % new_id_1) comment2 = createObject("plone.Comment") comment2.title = "Comment 2" comment2.text = "Comment text" comment2.Creator = "Joe" new_id_2 = conversation.addComment(comment2) self.comment2 = self.portal.doc1.restrictedTraverse("++conversation++default/%s" % new_id_2) comment3 = createObject("plone.Comment") comment3.title = "Comment 3" comment3.text = "Comment text" comment3.Creator = "Emma" new_id_3 = conversation.addComment(comment3) self.comment3 = self.portal.doc1.restrictedTraverse("++conversation++default/%s" % new_id_3) self.conversation = conversation
def test_move_comments_when_content_object_is_moved(self): # Create two folders and a content object with a comment self.portal.invokeFactory(id="folder1", title="Folder 1", type_name="Folder") self.portal.invokeFactory(id="folder2", title="Folder 2", type_name="Folder") self.portal.folder1.invokeFactory(id="moveme", title="Move Me", type_name="Document") conversation = IConversation(self.portal.folder1.moveme) comment = createObject("plone.Comment") comment_id = conversation.addComment(comment) # We need to commit here so that _p_jar isn't None and move will work transaction.savepoint(optimistic=True) # Move moveme from folder1 to folder2 cp = self.portal.folder1.manage_cutObjects(ids=("moveme",)) self.portal.folder2.manage_pasteObjects(cp) # Make sure no old comment brains are brains = self.catalog.searchResults( dict(portal_type="Discussion Item", path={"query": "/".join(self.portal.folder1.getPhysicalPath())}) ) self.assertEquals(len(brains), 0) brains = self.catalog.searchResults( dict(portal_type="Discussion Item", path={"query": "/".join(self.portal.folder2.getPhysicalPath())}) ) self.assertEquals(len(brains), 1) self.assertEquals(brains[0].getPath(), "/plone/folder2/moveme/++conversation++default/" + str(comment_id))
def test_move_comments_when_content_object_is_moved(self): # Create two folders and a content object with a comment self.portal.invokeFactory( id='folder1', title='Folder 1', type_name='Folder', ) self.portal.invokeFactory( id='folder2', title='Folder 2', type_name='Folder', ) self.portal.folder1.invokeFactory( id='moveme', title='Move Me', type_name='Document', ) conversation = IConversation(self.portal.folder1.moveme) comment = createObject('plone.Comment') comment_id = conversation.addComment(comment) # We need to commit here so that _p_jar isn't None and move will work transaction.savepoint(optimistic=True) # Move moveme from folder1 to folder2 cp = self.portal.folder1.manage_cutObjects(ids=('moveme', )) self.portal.folder2.manage_pasteObjects(cp) # Make sure no old comment brains are brains = self.catalog.searchResults( dict( portal_type='Discussion Item', path={ 'query': '/'.join(self.portal.folder1.getPhysicalPath()), }, ), ) self.assertEqual(len(brains), 0) brains = self.catalog.searchResults( dict( portal_type='Discussion Item', path={ 'query': '/'.join(self.portal.folder2.getPhysicalPath()), }, ), ) self.assertEqual(len(brains), 1) self.assertEqual( brains[0].getPath(), '/plone/folder2/moveme/++conversation++default/' + str(comment_id), )
def get_replies(self, workflow_actions=False): """Returns all replies to a content object. If workflow_actions is false, only published comments are returned. If workflow actions is true, comments are returned with workflow actions. """ context = aq_inner(self.context) conversation = IConversation(context, None) if conversation is None: return iter([]) wf = getToolByName(context, 'portal_workflow') # workflow_actions is only true when user # has 'Manage portal' permission def replies_with_workflow_actions(): # Generator that returns replies dict with workflow actions for r in conversation.getThreads(): comment_obj = r['comment'] # list all possible workflow actions actions = [ a for a in wf.listActionInfos(object=comment_obj) if a['category'] == 'workflow' and a['allowed'] ] r = r.copy() r['actions'] = actions yield r def published_replies(): # Generator that returns replies dict with workflow status. for r in conversation.getThreads(): comment_obj = r['comment'] workflow_status = wf.getInfoFor(comment_obj, 'review_state') if workflow_status == 'published': r = r.copy() r['workflow_status'] = workflow_status yield r # Return all direct replies if len(conversation.objectIds()): if workflow_actions: return replies_with_workflow_actions() else: return published_replies()
def test_add_anonymous_comment(self): self.portal.doc1.allow_discussion = True self.viewlet = CommentsViewlet(self.context, self.request, None, None) registry = queryUtility(IRegistry) settings = registry.forInterface(IDiscussionSettings, check=False) settings.anonymous_comments = True # Logout logout() def make_request(form={}): request = TestRequest() request.form.update(form) alsoProvides(request, IFormLayer) alsoProvides(request, IAttributeAnnotatable) return request provideAdapter(adapts=(Interface, IBrowserRequest), provides=Interface, factory=CommentForm, name=u'comment-form') # Post an anonymous comment and provide a name request = make_request(form={ 'form.widgets.name': u'john doe', 'form.widgets.text': u'bar' }) commentForm = getMultiAdapter( (self.context, request), name=u'comment-form' ) commentForm.update() data, errors = commentForm.extractData() # pylint: disable-msg=W0612 self.assertEqual(len(errors), 0) self.assertFalse(commentForm.handleComment(commentForm, 'action')) comments = IConversation(commentForm.context).getComments() comments = [comment for comment in comments] # consume itertor self.assertEqual(len(comments), 1) for comment in IConversation(commentForm.context).getComments(): self.assertEqual(comment.text, u'bar') self.assertIsNone(comment.creator) roles = comment.get_local_roles() self.assertEqual(len(roles), 0)
def test_delete_own_comment(self): """Delete own comment as logged-in user. """ # Allow discussion self.portal.doc1.allow_discussion = True self.viewlet = CommentsViewlet(self.context, self.request, None, None) def make_request(form={}): request = TestRequest() request.form.update(form) alsoProvides(request, IFormLayer) alsoProvides(request, IAttributeAnnotatable) return request provideAdapter(adapts=(Interface, IBrowserRequest), provides=Interface, factory=CommentForm, name=u"comment-form") # The form is submitted successfully, if the required text field is # filled out form_request = make_request(form={'form.widgets.text': u'bar'}) commentForm = getMultiAdapter((self.context, form_request), name=u"comment-form") commentForm.update() data, errors = commentForm.extractData() # pylint: disable-msg=W0612 self.assertEqual(len(errors), 0) self.assertFalse(commentForm.handleComment(commentForm, "foo")) # Delete the last comment conversation = IConversation(self.context) comment = [x for x in conversation.getComments()][-1] deleteView = getMultiAdapter((comment, self.request), name=u"delete-own-comment") # try to delete last comment with johndoe setRoles(self.portal, 'johndoe', ['Member']) login(self.portal, 'johndoe') self.assertRaises(Unauthorized, comment.restrictedTraverse, "@@delete-own-comment") self.assertEqual(1, len([x for x in conversation.getComments()])) # try to delete last comment with the same user that created it login(self.portal, TEST_USER_NAME) setRoles(self.portal, TEST_USER_ID, ['Member']) deleteView() self.assertEqual(0, len([x for x in conversation.getComments()]))
def test_commentators(self): # add and remove a few comments to make sure the commentators # property returns a true set # Create a conversation. In this case we doesn't assign it to an # object, as we just want to check the Conversation object API. conversation = IConversation(self.portal.doc1) self.assertEqual(conversation.total_comments, 0) # Add a four comments from three different users # Note: in real life, we always create # comments via the factory to allow different factories to be # swapped in comment1 = createObject('plone.Comment') comment1.text = 'Comment text' comment1.author_username = "******" conversation.addComment(comment1) comment2 = createObject('plone.Comment') comment2.text = 'Comment text' comment2.author_username = "******" conversation.addComment(comment2) comment3 = createObject('plone.Comment') comment3.text = 'Comment text' comment3.author_username = "******" new_comment3_id = conversation.addComment(comment3) comment4 = createObject('plone.Comment') comment4.text = 'Comment text' comment4.author_username = "******" new_comment4_id = conversation.addComment(comment4) # check if all commentators are in the commentators list self.assertEqual(conversation.total_comments, 4) self.assertTrue('Jim' in conversation.commentators) self.assertTrue('Joe' in conversation.commentators) self.assertTrue('Jack' in conversation.commentators) # remove the comment from Jack del conversation[new_comment3_id] # check if Jack is still in the commentators list (since # he had added two comments) self.assertTrue('Jim' in conversation.commentators) self.assertTrue('Joe' in conversation.commentators) self.assertTrue('Jack' in conversation.commentators) self.assertEqual(conversation.total_comments, 3) # remove the second comment from Jack del conversation[new_comment4_id] # check if Jack has been removed from the commentators list self.assertTrue('Jim' in conversation.commentators) self.assertTrue('Joe' in conversation.commentators) self.assertFalse('Jack' in conversation.commentators) self.assertEqual(conversation.total_comments, 2)
def setUp(self): # Setup sandbox self.portal = self.layer['portal'] self.request = self.layer['request'] setRoles(self.portal, TEST_USER_ID, ['Manager']) name = self.portal.invokeFactory(id='doc1', title='Document 1', type_name='Document') self.document = self.portal[name] conversation = IConversation(self.document) replies = IReplies(conversation) comment = createObject('plone.Comment') comment.text = 'This is a comment' new_id = replies.addComment(comment) comment = self.document.restrictedTraverse( '++conversation++default/%s' % new_id) re_comment = createObject('plone.Comment') re_comment.text = 'This is a reply' re_comment.author_username = "******" re_comment.author_name = "Juliana" re_comment.author_email = "*****@*****.**" replies = IReplies(comment) new_re_id = replies.addComment(re_comment)
def test_no_comment(self): IConversation(self.portal.doc1) # Make sure no conversation has been created self.assertTrue( 'plone.app.discussion:conversation' not in IAnnotations(self.portal.doc1), )
def setUp(self): self.app = self.layer["app"] self.portal = self.layer["portal"] self.request = self.layer["request"] self.portal_url = self.portal.absolute_url() # Allow discussion registry = getUtility(IRegistry) settings = registry.forInterface(IDiscussionSettings, check=False) settings.globally_enabled = True settings.edit_comment_enabled = True settings.delete_own_comment_enabled = True # doc with comments self.doc = api.content.create( container=self.portal, type="Document", id="doc_with_comments", title="Document with comments", allow_discussion=True, ) self.conversation = IConversation(self.doc) self.replies = IReplies(self.conversation) comment = createObject("plone.Comment") comment.text = "Comment" self.comment = self.replies[self.replies.addComment(comment)] comment = createObject("plone.Comment") comment.text = "Comment 2" self.replies.addComment(comment)
def commentCount(self, ob): if USE_PAD: conversation = IConversation(ob) return len(conversation) else: discussion = self.portal_discussion.getDiscussionFor(ob) return discussion.replyCount(ob)
def reply(self): # Disable CSRF protection if "IDisableCSRFProtection" in dir(plone.protect.interfaces): alsoProvides(self.request, plone.protect.interfaces.IDisableCSRFProtection) conversation = IConversation(self.context) if self.comment_id and self.comment_id not in list(conversation): self.request.response.setStatus(404) return # Fake request data body = json_body(self.request) for key, value in body.items(): self.request.form["form.widgets." + key] = value form = CommentForm(self.context, self.request) form.update() action = form.actions["comment"] data, errors = form.extractData() if errors: raise BadRequest({"errors": [err.error for err in errors]}) form.handleComment(form=form, action=action) fix_location_header(self.context, self.request) return self.reply_no_content()
def reply(self): if not self.comment_id: raise BadRequest("Comment id is a required part of the url") conversation = IConversation(self.context) if self.comment_id not in list(conversation): self.request.response.setStatus(404) return comment = conversation[self.comment_id] # Permission checks if not (edit_comment_allowed() and can_edit(comment)): raise Unauthorized() # Fake request data body = json_body(self.request) for key, value in body.items(): self.request.form["form.widgets." + key] = value form = EditCommentForm(comment, self.request) form.__parent__ = form.context.__parent__.__parent__ form.update() action = form.actions["comment"] data, errors = form.extractData() if errors: raise BadRequest({"errors": [err.error for err in errors]}) comment.modification_date = datetime.utcnow() form.handleComment(form=form, action=action) fix_location_header(self.context, self.request) return self.reply_no_content()
def test_delete_comment(self): # Add and remove a comment to a CommentReplies adapter # Create a conversation. In this case we doesn't assign it to an # object, as we just want to check the Conversation object API. conversation = IConversation(self.portal.doc1) # Add a comment to the conversation replies = IReplies(conversation) comment = createObject('plone.Comment') comment.text = 'Comment text' new_id = replies.addComment(comment) comment = self.portal.doc1.restrictedTraverse( '++conversation++default/%s' % new_id) # Add a reply to the CommentReplies adapter of the first comment re_comment = createObject('plone.Comment') re_comment.text = 'Comment text' replies = IReplies(comment) new_re_id = replies.addComment(re_comment) # Remove the reply to the CommentReplies adapter del replies[new_re_id] # Make sure there is no comment left in CommentReplies self.assertEqual(len(replies), 0) # Make sure the first comment is still in the conversation self.assertEqual(conversation.total_comments, 1)
def test_add_comment(self): # Add comments to a CommentReplies adapter # Create a conversation. In this case we doesn't assign it to an # object, as we just want to check the Conversation object API. conversation = IConversation(self.portal.doc1) # Add a comment to the conversation replies = IReplies(conversation) comment = createObject('plone.Comment') comment.text = 'Comment text' new_id = replies.addComment(comment) comment = self.portal.doc1.restrictedTraverse( '++conversation++default/{0}'.format(new_id), ) # Add a reply to the CommentReplies adapter of the first comment re_comment = createObject('plone.Comment') re_comment.text = 'Comment text' replies = IReplies(comment) new_re_id = replies.addComment(re_comment) # check that replies provides the IReplies interface self.assertTrue(IReplies.providedBy(replies)) # Make sure our comment was added self.assertTrue(new_re_id in replies) # Make sure it is also reflected in the conversation self.assertTrue(new_re_id in conversation) # Make sure the conversation has the correct comment id self.assertEqual(conversation[new_re_id].comment_id, new_re_id)
def setUp(self): # Setup sandbox self.portal = self.layer['portal'] self.request = self.layer['request'] setRoles(self.portal, TEST_USER_ID, ['Manager']) self.document = self.portal['doc1'] conversation = IConversation(self.document) replies = IReplies(conversation) comment = createObject('plone.Comment') comment.text = 'This is a comment' new_id = replies.addComment(comment) comment = self.document.restrictedTraverse( '++conversation++default/{0}'.format(new_id) ) re_comment = createObject('plone.Comment') re_comment.text = 'This is a reply' re_comment.author_username = '******' re_comment.author_name = 'Juliana' re_comment.author_email = '*****@*****.**' replies = IReplies(comment) replies.addComment(re_comment)
def test_move_upper_level_folder(self): # create a folder with a nested structure self.portal.invokeFactory(id='sourcefolder', title='Source Folder', type_name='Folder') self.portal.sourcefolder.invokeFactory(id='moveme', title='Move Me', type_name='Folder') self.portal.sourcefolder.moveme.invokeFactory(id='mydocument', title='My Document', type_name='Folder') self.portal.invokeFactory(id='targetfolder', title='Target Folder', type_name='Folder') # create comment on my-document conversation = IConversation( self.portal.sourcefolder.moveme.mydocument ) comment = createObject('plone.Comment') comment_id = conversation.addComment(comment) # We need to commit here so that _p_jar isn't None and move will work transaction.savepoint(optimistic=True) # Move moveme from folder1 to folder2 cp = self.portal.sourcefolder.manage_cutObjects(ids=('moveme',)) self.portal.targetfolder.manage_pasteObjects(cp) # Make sure no old comment brains are left brains = self.catalog.searchResults(dict( portal_type="Discussion Item", path={'query': '/plone/sourcefolder/moveme'} )) self.assertEqual(len(brains), 0) # make sure comments are correctly index on the target brains = self.catalog.searchResults(dict( portal_type="Discussion Item", path={'query': '/plone/targetfolder/moveme'} )) self.assertEqual(len(brains), 1) self.assertEqual( brains[0].getPath(), '/plone/targetfolder/moveme/mydocument/++conversation++default/' + str(comment_id) )
def test_add_comment(self): """Post a comment as logged-in user. """ # Allow discussion self.discussionTool.overrideDiscussionFor(self.portal.doc1, True) self.viewlet = CommentsViewlet(self.context, self.request, None, None) def make_request(form={}): request = TestRequest() request.form.update(form) alsoProvides(request, IFormLayer) alsoProvides(request, IAttributeAnnotatable) return request provideAdapter( adapts=(Interface, IBrowserRequest), provides=Interface, factory=CommentForm, name=u"comment-form" ) # The form should return an error if the comment text field is empty request = make_request(form={}) commentForm = getMultiAdapter( (self.context, request), name=u"comment-form" ) commentForm.update() data, errors = commentForm.extractData() # pylint: disable-msg=W0612 self.assertEqual(len(errors), 1) self.assertFalse(commentForm.handleComment(commentForm, "foo")) # The form is submitted successfully, if the required text field is # filled out request = make_request(form={'form.widgets.text': u'bar'}) commentForm = getMultiAdapter( (self.context, request), name=u"comment-form" ) commentForm.update() data, errors = commentForm.extractData() # pylint: disable-msg=W0612 self.assertEqual(len(errors), 0) self.assertFalse(commentForm.handleComment(commentForm, "foo")) comments = IConversation(commentForm.context).getComments() comments = [comment for comment in comments] # consume itertor self.assertEqual(len(comments), 1) comment = comments[0] self.assertEqual(comment.text, u"bar") self.assertEqual(comment.creator, "test-user") self.assertEqual(comment.getOwner().getUserName(), "test-user") local_roles = comment.get_local_roles() self.assertTrue(('test-user', ('Owner',)) in local_roles)
def _comments(obj): from plone.app.discussion.interfaces import IConversation conversation = IConversation(obj) comments = [] for c in conversation.getComments(): comments.append({ 'author_username': c.author_username, 'text': c.text, 'comment_id': c.comment_id, 'creation_date': api.portal.get_localized_time(c.creation_date, long_format=True) }) return comments
def test_get_commenter_home_url(self): comment = createObject('plone.Comment') comment.text = 'Comment text' IConversation(self.portal.doc1) portal_membership = getToolByName(self.portal, 'portal_membership') m = portal_membership.getAuthenticatedMember() self.assertEqual(self.viewlet.get_commenter_home_url(m.getUserName()), 'http://nohost/plone/author/test-user')
def test_delete_comment(self): # Create a conversation. In this case we doesn't assign it to an # object, as we just want to check the Conversation object API. conversation = IConversation(self.portal.doc1) # Add a comment. Note: in real life, we always create comments via the # factory to allow different factories to be swapped in comment = createObject('plone.Comment') comment.text = 'Comment text' new_id = conversation.addComment(comment) # make sure the comment has been added self.assertEqual(len(list(conversation.getComments())), 1) self.assertEqual(len(tuple(conversation.getThreads())), 1) self.assertEqual(conversation.total_comments, 1) # delete the comment we just created del conversation[new_id] # make sure there is no comment left in the conversation self.assertEqual(len(list(conversation.getComments())), 0) self.assertEqual(len(tuple(conversation.getThreads())), 0) self.assertEqual(conversation.total_comments, 0)
def get_discussion_count(self): try: # plone.app.discussion.conversation object # fetched via IConversation adapter conversation = IConversation(self) except Exception: return 0 else: return conversation.total_comments
def conclusion_reply_number(context): replynum = 0 conclusions = context.values(['Conclusions']) if conclusions: conclusion = conclusions[0] disc = IConversation(conclusion) return disc.total_comments return replynum
def test_migrate_comment(self): # Create a comment talkback = self.discussion.getDiscussionFor(self.doc) self.doc.talkback.createReply('My Title', 'My Text', Creator='Jim') reply = talkback.getReplies()[0] reply.setReplyTo(self.doc) reply.creation_date = DateTime(2003, 3, 11, 9, 28, 6, 'GMT') reply.modification_date = DateTime(2009, 7, 12, 19, 38, 7, 'GMT') self._publish(reply) self.assertEqual(reply.Title(), 'My Title') self.assertEqual(reply.EditableBody(), 'My Text') self.assertTrue('Jim' in reply.listCreators()) self.assertEqual(talkback.replyCount(self.doc), 1) self.assertEqual(reply.inReplyTo(), self.doc) # Call migration script self.view() # Make sure a conversation has been created self.assertTrue( 'plone.app.discussion:conversation' in IAnnotations(self.doc)) conversation = IConversation(self.doc) # Check migration self.assertEqual(conversation.total_comments, 1) self.assertTrue(conversation.getComments().next()) comment1 = conversation.values()[0] self.assertTrue(IComment.providedBy(comment1)) self.assertEqual(comment1.Title(), 'My Title') self.assertEqual(comment1.text, '<p>My Text</p>\n') self.assertEqual(comment1.mime_type, 'text/html') self.assertEqual(comment1.Creator(), 'Jim') self.assertEqual(comment1.creation_date, datetime(2003, 3, 11, 9, 28, 6)) self.assertEqual(comment1.modification_date, datetime(2009, 7, 12, 19, 38, 7)) self.assertEqual([{ 'comment': comment1, 'depth': 0, 'id': long(comment1.id) }], list(conversation.getThreads())) self.assertFalse(self.doc.talkback)
class RedirectionTest(unittest.TestCase): layer = PLONE_APP_DISCUSSION_INTEGRATION_TESTING def setUp(self): # Update settings. self.portal = self.layer['portal'] self.request = self.layer['request'] setRoles(self.portal, TEST_USER_ID, ['Manager']) # applyProfile(self.portal, 'plone.app.discussion:default') registry = queryUtility(IRegistry) settings = registry.forInterface(IDiscussionSettings) settings.globally_enabled = True self.portal.portal_workflow.setChainForPortalTypes( ('Discussion Item', ), ('comment_review_workflow', ), ) # Create page plus comment. self.portal.invokeFactory( id='page', title='Page 1', type_name='Document', ) self.page = self.portal.page self.conversation = IConversation(self.page) comment = createObject('plone.Comment') comment.text = 'Comment text' self.comment_id = self.conversation.addComment(comment) self.comment = list(self.conversation.getComments())[0] def test_regression(self): page_url = self.page.absolute_url() self.request['HTTP_REFERER'] = page_url for Klass in (DeleteComment, PublishComment): view = Klass(self.comment, self.request) view.__parent__ = self.comment self.assertEqual(page_url, view()) def test_valid_next_url(self): self.request['HTTP_REFERER'] = 'http://attacker.com' for Klass in (DeleteComment, PublishComment): view = Klass(self.comment, self.request) view.__parent__ = self.comment self.assertNotEqual('http://attacker.com', view())
def get_replies(self, workflow_actions=False): context = aq_inner(self.context) conversation = IConversation(context) wf = getToolByName(context, 'portal_workflow') def replies_with_workflow_actions(): # Generator that returns replies dict with workflow actions for comment_obj in conversation.getComments(): # list all possible workflow actions actions = [ a for a in wf.listActionInfos(object=comment_obj) if a['category'] == 'workflow' and a['allowed'] ] yield {'comment': comment_obj, 'actions': actions} if len(conversation.objectIds()) > 0: return replies_with_workflow_actions()
def test_tool_indexing(self): # Create a conversation. In this case we doesn't assign it to an # object, as we just want to check the Conversation object API. conversation = IConversation(self.portal.doc1) # Add a comment. comment = createObject('plone.Comment') comment.creator = 'Jim' comment.text = 'Comment text' conversation.addComment(comment) # Check that the comment got indexed in the tool: tool = queryUtility(ICommentingTool) comment = list(tool.searchResults()) self.assertTrue( len(comment) == 1, "There is only one comment, but we got" " %s results in the search" % len(comment)) self.assertEqual(comment[0].Title, 'Jim on Document 1')
def get_discussion(obj, path): conversation = IConversation(obj, None) if not conversation: return serializer = getMultiAdapter((conversation, self.request), ISerializeToJson) output = serializer() if output: results.append({"uuid": IUUID(obj), "conversation": output}) return
def test_get_replies_with_workflow_actions(self): self.assertFalse(self.viewlet.get_replies(workflow_actions=True)) comment = createObject('plone.Comment') comment.text = 'Comment text' conversation = IConversation(self.portal.doc1) c1 = conversation.addComment(comment) self.assertEqual( len(tuple(self.viewlet.get_replies(workflow_actions=True))), 1) # Enable moderation workflow self.workflowTool.setChainForPortalTypes(('Discussion Item', ), ('comment_review_workflow,')) # Check if workflow actions are available reply = self.viewlet.get_replies(workflow_actions=True).next() self.assertTrue('actions' in reply) self.assertEqual(reply['actions'][0]['id'], 'publish') self.assertEqual( reply['actions'][0]['url'], 'http://nohost/plone/doc1/++conversation++default/%s' % int(c1) + '/content_status_modify?workflow_action=publish')
def reply(self): conversation = IConversation(self.context) if not self.comment_id: serializer = getMultiAdapter((conversation, self.request), ISerializeToJson) else: comment = conversation[self.comment_id] serializer = getMultiAdapter((comment, self.request), ISerializeToJson) return serializer()
def test_traversal(self): # make sure we can traverse to conversations and get a URL and path conversation = self.portal.doc1.restrictedTraverse( '++conversation++default') self.assertTrue(IConversation.providedBy(conversation)) self.assertEqual(('', 'plone', 'doc1', '++conversation++default'), conversation.getPhysicalPath()) self.assertEqual('http://nohost/plone/doc1/++conversation++default', conversation.absolute_url())
def test_parent(self): # Check that conversation has a content object as parent # Create a conversation. conversation = IConversation(self.portal.doc1) # Check the parent self.assertTrue(conversation.__parent__) self.assertTrue(aq_parent(conversation)) self.assertEqual(conversation.__parent__.getId(), 'doc1')