enabled property code added to conversation.

svn path=/plone.app.discussion/trunk/; revision=27380
This commit is contained in:
Timo Stollenwerk 2009-06-11 10:14:44 +00:00
parent 0f7d05566a
commit e465d73c95
2 changed files with 136 additions and 21 deletions

View File

@ -14,14 +14,18 @@ import time
from persistent import Persistent 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.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.annotation.interfaces import IAnnotations, IAnnotatable
from zope.event import notify from zope.event import notify
from Acquisition import aq_base from Acquisition import aq_base, aq_inner
from Acquisition import Explicit from Acquisition import Explicit
from OFS.Traversable import Traversable from OFS.Traversable import Traversable
@ -29,6 +33,8 @@ from OFS.Traversable import Traversable
from OFS.event import ObjectWillBeAddedEvent from OFS.event import ObjectWillBeAddedEvent
from OFS.event import ObjectWillBeRemovedEvent from OFS.event import ObjectWillBeRemovedEvent
from Products.CMFCore.utils import getToolByName
from zope.app.container.contained import ContainerModifiedEvent from zope.app.container.contained import ContainerModifiedEvent
from zope.app.container.contained import ObjectAddedEvent from zope.app.container.contained import ObjectAddedEvent
@ -44,7 +50,7 @@ except ImportError:
from BTrees.OOBTree import OOBTree as LOBTree from BTrees.OOBTree import OOBTree as LOBTree
from BTrees.OOBTree import OOSet as LLSet 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 from plone.app.discussion.comment import Comment
ANNOTATION_KEY = 'plone.app.discussion:conversation' ANNOTATION_KEY = 'plone.app.discussion:conversation'
@ -80,7 +86,29 @@ class Conversation(Traversable, Persistent, Explicit):
@property @property
def enabled(self): 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 return True
@property @property
@ -104,7 +132,7 @@ class Conversation(Traversable, Persistent, Explicit):
count = 0l count = 0l
for comment in self._comments.values(min=start): for comment in self._comments.values(min=start):
yield comment yield comment
count += 1 count += 1
if size and count > size: if size and count > size:
return return
@ -112,11 +140,11 @@ class Conversation(Traversable, Persistent, Explicit):
def getThreads(self, start=0, size=None, root=0, depth=None): def getThreads(self, start=0, size=None, root=0, depth=None):
"""Get threaded comments """Get threaded comments
""" """
def recurse(comment_id, d=0): def recurse(comment_id, d=0):
# Yield the current comment before we look for its children # Yield the current comment before we look for its children
yield {'id': comment_id, 'comment': self._comments[comment_id], 'depth': d} yield {'id': comment_id, 'comment': self._comments[comment_id], 'depth': d}
# Recurse if there are children and we are not out of our depth # Recurse if there are children and we are not out of our depth
if depth is None or d + 1 < depth: if depth is None or d + 1 < depth:
children = self._children.get(comment_id, None) children = self._children.get(comment_id, None)
@ -124,18 +152,18 @@ class Conversation(Traversable, Persistent, Explicit):
for child_id in children: for child_id in children:
for value in recurse(child_id, d+1): for value in recurse(child_id, d+1):
yield value yield value
# Find top level threads # Find top level threads
comments = self._children.get(root, None) comments = self._children.get(root, None)
if comments is not None: if comments is not None:
count = 0l count = 0l
for comment_id in comments.keys(min=start): for comment_id in comments.keys(min=start):
# Abort if we have found all the threads we want # Abort if we have found all the threads we want
count += 1 count += 1
if size and count > size: if size and count > size:
return return
# Let the closure recurse # Let the closure recurse
for value in recurse(comment_id): for value in recurse(comment_id):
yield value yield value
@ -211,14 +239,14 @@ class Conversation(Traversable, Persistent, Explicit):
for child_id in self._children.get(key, []): for child_id in self._children.get(key, []):
# avoid sending ContainerModifiedEvent multiple times # avoid sending ContainerModifiedEvent multiple times
self.__delitem__(child_id, suppress_container_modified=True) self.__delitem__(child_id, suppress_container_modified=True)
# XXX: During the events sent from the recursive deletion, the # XXX: During the events sent from the recursive deletion, the
# _children data structure may be in an inconsistent state. We may # _children data structure may be in an inconsistent state. We may
# need to delay sending the events until it is fixed up. # need to delay sending the events until it is fixed up.
# Remove the comment from _comments # Remove the comment from _comments
self._comments.pop(key) self._comments.pop(key)
# Remove this comment as a child of its parent # Remove this comment as a child of its parent
if not suppress_container_modified: if not suppress_container_modified:
parent = comment.in_reply_to parent = comment.in_reply_to
@ -226,16 +254,16 @@ class Conversation(Traversable, Persistent, Explicit):
parent_children = self._children.get(parent, None) parent_children = self._children.get(parent, None)
if parent_children is not None and key in parent_children: if parent_children is not None and key in parent_children:
parent_children.remove(key) parent_children.remove(key)
# Remove commentators # Remove commentators
if commentator and commentator in self._commentators: if commentator and commentator in self._commentators:
if self._commentators[commentator] <= 1: if self._commentators[commentator] <= 1:
del self._commentators[commentator] del self._commentators[commentator]
else: else:
self._commentators[commentator] -= 1 self._commentators[commentator] -= 1
notify(ObjectRemovedEvent(comment, self, key)) notify(ObjectRemovedEvent(comment, self, key))
if not suppress_container_modified: if not suppress_container_modified:
notify(ContainerModifiedEvent(self)) notify(ContainerModifiedEvent(self))
@ -279,6 +307,7 @@ def conversationAdapterFactory(content):
conversation = Conversation() conversation = Conversation()
annotions[ANNOTATION_KEY] = conversation annotions[ANNOTATION_KEY] = conversation
conversation = annotions[ANNOTATION_KEY] conversation = annotions[ANNOTATION_KEY]
conversation.__parent__ = aq_base(content)
return conversation return conversation
class ConversationReplies(object): class ConversationReplies(object):
@ -350,13 +379,13 @@ class ConversationReplies(object):
def iteritems(self): def iteritems(self):
for key in self.children: for key in self.children:
yield (key, self.conversation[key],) yield (key, self.conversation[key],)
@property @property
def children(self): def children(self):
# we need to look this up every time, because we may not have a # we need to look this up every time, because we may not have a
# dict yet when the adapter is first created # dict yet when the adapter is first created
return self.conversation._children.get(self.comment_id, LLSet()) return self.conversation._children.get(self.comment_id, LLSet())
class CommentReplies(ConversationReplies): class CommentReplies(ConversationReplies):
"""An IReplies adapter for comments. """An IReplies adapter for comments.

View File

@ -1,14 +1,14 @@
import unittest import unittest
from datetime import datetime, timedelta from datetime import datetime, timedelta
from plone.registry import Registry from zope.component import createObject, queryUtility
from zope.component import createObject
from Acquisition import aq_base, aq_parent, aq_inner from Acquisition import aq_base, aq_parent, aq_inner
from plone.app.vocabularies.types import BAD_TYPES from plone.app.vocabularies.types import BAD_TYPES
from plone.registry.interfaces import IRegistry
from Products.CMFCore.utils import getToolByName from Products.CMFCore.utils import getToolByName
from Products.PloneTestCase.ptc import PloneTestCase from Products.PloneTestCase.ptc import PloneTestCase
from plone.app.discussion.tests.layer import DiscussionLayer 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}, {'comment': comment2_1, 'depth': 1, 'id': new_id_2_1},
], list(conversation.getThreads())) ], 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): def test_dict_operations(self):
# test dict operations and acquisition wrapping # 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', '++conversation++default'), conversation.getPhysicalPath())
self.assertEquals('plone/doc1/%2B%2Bconversation%2B%2Bdefault', conversation.absolute_url()) 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): def test_discussion_item_not_in_bad_types(self):
self.failIf('Discussion Item' in BAD_TYPES) self.failIf('Discussion Item' in BAD_TYPES)