From e465d73c958f46a9486f690e701f9303d1d65b7a Mon Sep 17 00:00:00 2001 From: Timo Stollenwerk Date: Thu, 11 Jun 2009 10:14:44 +0000 Subject: [PATCH] enabled property code added to conversation. svn path=/plone.app.discussion/trunk/; revision=27380 --- plone/app/discussion/conversation.py | 65 +++++++++---- .../app/discussion/tests/test_conversation.py | 92 ++++++++++++++++++- 2 files changed, 136 insertions(+), 21 deletions(-) diff --git a/plone/app/discussion/conversation.py b/plone/app/discussion/conversation.py index 1a3de77..66762f9 100644 --- a/plone/app/discussion/conversation.py +++ b/plone/app/discussion/conversation.py @@ -14,14 +14,18 @@ import time from persistent import Persistent +from plone.registry.interfaces import IRegistry + +from zope.app.component.hooks import getSite + from zope.interface import implements, implementer -from zope.component import adapts, adapter +from zope.component import adapts, adapter, queryUtility from zope.annotation.interfaces import IAnnotations, IAnnotatable from zope.event import notify -from Acquisition import aq_base +from Acquisition import aq_base, aq_inner from Acquisition import Explicit from OFS.Traversable import Traversable @@ -29,6 +33,8 @@ from OFS.Traversable import Traversable from OFS.event import ObjectWillBeAddedEvent from OFS.event import ObjectWillBeRemovedEvent +from Products.CMFCore.utils import getToolByName + from zope.app.container.contained import ContainerModifiedEvent from zope.app.container.contained import ObjectAddedEvent @@ -44,7 +50,7 @@ except ImportError: from BTrees.OOBTree import OOBTree as LOBTree from BTrees.OOBTree import OOSet as LLSet -from plone.app.discussion.interfaces import IConversation, IReplies +from plone.app.discussion.interfaces import IConversation, IReplies, IDiscussionSettings from plone.app.discussion.comment import Comment ANNOTATION_KEY = 'plone.app.discussion:conversation' @@ -80,7 +86,29 @@ class Conversation(Traversable, Persistent, Explicit): @property def enabled(self): - # TODO - check __parent__'s settings + global settings + # Returns True if discussion is allowed + + # Fetch discussion registry + registry = queryUtility(IRegistry) + settings = registry.for_interface(IDiscussionSettings) + + # Check if discussion is allowed globally + if not settings.globally_enabled: + return False + + # Check if discussion is allowed on the content type + site = getSite() + portal_types = getToolByName(site, 'portal_types') + portal_type = self.__parent__.portal_type + document_fti = getattr(portal_types, 'Document') + if not document_fti.getProperty('allow_discussion'): + # If discussion is not allowed on the content type, + # check if 'allow discussion' is overridden on the content object. + #if hasattr( aq_base(self.__parent__), 'allow_discussion' ): + portal_discussion = getToolByName(site, 'portal_discussion') + if not portal_discussion.isDiscussionAllowedFor(aq_inner(self.__parent__)): + return False + return True @property @@ -104,7 +132,7 @@ class Conversation(Traversable, Persistent, Explicit): count = 0l for comment in self._comments.values(min=start): yield comment - + count += 1 if size and count > size: return @@ -112,11 +140,11 @@ class Conversation(Traversable, Persistent, Explicit): def getThreads(self, start=0, size=None, root=0, depth=None): """Get threaded comments """ - + def recurse(comment_id, d=0): # Yield the current comment before we look for its children yield {'id': comment_id, 'comment': self._comments[comment_id], 'depth': d} - + # Recurse if there are children and we are not out of our depth if depth is None or d + 1 < depth: children = self._children.get(comment_id, None) @@ -124,18 +152,18 @@ class Conversation(Traversable, Persistent, Explicit): for child_id in children: for value in recurse(child_id, d+1): yield value - + # Find top level threads comments = self._children.get(root, None) if comments is not None: count = 0l for comment_id in comments.keys(min=start): - + # Abort if we have found all the threads we want count += 1 if size and count > size: return - + # Let the closure recurse for value in recurse(comment_id): yield value @@ -211,14 +239,14 @@ class Conversation(Traversable, Persistent, Explicit): for child_id in self._children.get(key, []): # avoid sending ContainerModifiedEvent multiple times self.__delitem__(child_id, suppress_container_modified=True) - + # XXX: During the events sent from the recursive deletion, the # _children data structure may be in an inconsistent state. We may # need to delay sending the events until it is fixed up. - + # Remove the comment from _comments self._comments.pop(key) - + # Remove this comment as a child of its parent if not suppress_container_modified: parent = comment.in_reply_to @@ -226,16 +254,16 @@ class Conversation(Traversable, Persistent, Explicit): parent_children = self._children.get(parent, None) if parent_children is not None and key in parent_children: parent_children.remove(key) - + # Remove commentators if commentator and commentator in self._commentators: if self._commentators[commentator] <= 1: del self._commentators[commentator] else: self._commentators[commentator] -= 1 - + notify(ObjectRemovedEvent(comment, self, key)) - + if not suppress_container_modified: notify(ContainerModifiedEvent(self)) @@ -279,6 +307,7 @@ def conversationAdapterFactory(content): conversation = Conversation() annotions[ANNOTATION_KEY] = conversation conversation = annotions[ANNOTATION_KEY] + conversation.__parent__ = aq_base(content) return conversation class ConversationReplies(object): @@ -350,13 +379,13 @@ class ConversationReplies(object): def iteritems(self): for key in self.children: yield (key, self.conversation[key],) - + @property def children(self): # we need to look this up every time, because we may not have a # dict yet when the adapter is first created return self.conversation._children.get(self.comment_id, LLSet()) - + class CommentReplies(ConversationReplies): """An IReplies adapter for comments. diff --git a/plone/app/discussion/tests/test_conversation.py b/plone/app/discussion/tests/test_conversation.py index 7a48c62..2d79d01 100644 --- a/plone/app/discussion/tests/test_conversation.py +++ b/plone/app/discussion/tests/test_conversation.py @@ -1,14 +1,14 @@ import unittest from datetime import datetime, timedelta -from plone.registry import Registry - -from zope.component import createObject +from zope.component import createObject, queryUtility from Acquisition import aq_base, aq_parent, aq_inner from plone.app.vocabularies.types import BAD_TYPES +from plone.registry.interfaces import IRegistry + from Products.CMFCore.utils import getToolByName from Products.PloneTestCase.ptc import PloneTestCase from plone.app.discussion.tests.layer import DiscussionLayer @@ -155,6 +155,82 @@ class ConversationTest(PloneTestCase): {'comment': comment2_1, 'depth': 1, 'id': new_id_2_1}, ], list(conversation.getThreads())) + def test_disable_commenting_globally(self): + + # Create a conversation. + conversation = IConversation(self.portal.doc1) + + # We have to allow discussion on Document content type, since + # otherwise allow_discussion will always return False + portal_types = getToolByName(self.portal, 'portal_types') + document_fti = getattr(portal_types, 'Document') + document_fti.manage_changeProperties(allow_discussion = True) + + # Check if conversation is enabled now + self.assertEquals(conversation.enabled, True) + + # Disable commenting in the registry + registry = queryUtility(IRegistry) + settings = registry.for_interface(IDiscussionSettings) + settings.globally_enabled = False + + # Check if commenting is disabled on the conversation + self.assertEquals(conversation.enabled, False) + + # Enable discussion again + settings.globally_enabled = True + self.assertEquals(conversation.enabled, True) + + + def test_disable_commenting_for_content_type(self): + + # Create a conversation. + conversation = IConversation(self.portal.doc1) + + # The Document content type is disabled by default + self.assertEquals(conversation.enabled, False) + + # Allow discussion on Document content type + portal_types = getToolByName(self.portal, 'portal_types') + document_fti = getattr(portal_types, 'Document') + document_fti.manage_changeProperties(allow_discussion = True) + + # Check if conversation is enabled now + self.assertEquals(conversation.enabled, True) + + # Disallow discussion on Document content type + portal_types = getToolByName(self.portal, 'portal_types') + document_fti = getattr(portal_types, 'Document') + document_fti.manage_changeProperties(allow_discussion = False) + + # Check if conversation is enabled now + self.assertEquals(conversation.enabled, False) + + def test_is_discussion_allowed_for_folder(self): + # Create a folder with two content objects. Change allow_discussion + # and check if the content objects inside the folder are commentable. + pass + + def test_is_discussion_allowed_on_content_object(self): + # Allow discussion on a single content object + + registry = queryUtility(IRegistry) + settings = registry.for_interface(IDiscussionSettings) + + # Create a conversation. + conversation = IConversation(self.portal.doc1) + + # Discussion is disallowed by default + self.assertEquals(conversation.enabled, False) + + # Allow discussion on content object + self.portal_discussion.overrideDiscussionFor(self.portal.doc1, True) + + # Check if discussion is now allowed on the content object + self.assertEquals(conversation.enabled, True) + + self.portal_discussion.overrideDiscussionFor(self.portal.doc1, False) + self.assertEquals(conversation.enabled, False) def test_dict_operations(self): # test dict operations and acquisition wrapping @@ -459,6 +535,16 @@ class ConversationTest(PloneTestCase): self.assertEquals(('', 'plone', 'doc1', '++conversation++default'), conversation.getPhysicalPath()) self.assertEquals('plone/doc1/%2B%2Bconversation%2B%2Bdefault', 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.failUnless(conversation.__parent__) + self.assertEquals(conversation.__parent__.getId(), 'doc1') + def test_discussion_item_not_in_bad_types(self): self.failIf('Discussion Item' in BAD_TYPES)