Move some policy out of the conversation storage adapter into a view,

specifically "enabled()".  Prevents having to replace/migrate
persistent objects to change policy which really only concerns the
context and possibly the request, not the conversation storage. Fixes
#11372.

svn path=/plone.app.discussion/trunk/; revision=48849
This commit is contained in:
Ross Patterson 2011-04-15 04:29:46 +00:00
parent 7354ca4298
commit 3708429a37
8 changed files with 146 additions and 104 deletions

View File

@ -4,6 +4,13 @@ Changelog
2.0b2 (Unreleased) 2.0b2 (Unreleased)
------------------ ------------------
- Move some policy out of the conversation storage adapter into a
view, specifically "enabled()". Prevents having to replace/migrate
persistent objects to change policy which really only concerns the
context and possibly the request, not the conversation storage.
Fixes #11372.
[rossp]
- Fix unindexing of comments when deleting content resulting from - Fix unindexing of comments when deleting content resulting from
iterating over a BTree while modifying it. Fixes #11402. iterating over a BTree while modifying it. Fixes #11402.
[rossp] [rossp]

View File

@ -168,14 +168,15 @@ class CommentForm(extensible.ExtensibleForm, form.Form):
if 'user_notification' in data: if 'user_notification' in data:
user_notification = data['user_notification'] user_notification = data['user_notification']
# The add-comment view is called on the conversation object
conversation = IConversation(self.__parent__)
# Check if conversation is enabled on this content object # Check if conversation is enabled on this content object
if not conversation.enabled(): if not self.__parent__.restrictedTraverse(
'@@conversation_view').enabled():
raise Unauthorized, "Discussion is not enabled for this content\ raise Unauthorized, "Discussion is not enabled for this content\
object." object."
# The add-comment view is called on the conversation object
conversation = IConversation(self.__parent__)
if data['in_reply_to']: if data['in_reply_to']:
# Fetch the comment we want to reply to # Fetch the comment we want to reply to
conversation_to_reply_to = conversation.get(data['in_reply_to']) conversation_to_reply_to = conversation.get(data['in_reply_to'])
@ -291,8 +292,7 @@ class CommentsViewlet(ViewletBase):
def is_discussion_allowed(self): def is_discussion_allowed(self):
context = aq_inner(self.context) context = aq_inner(self.context)
conversation = IConversation(context) return context.restrictedTraverse('@@conversation_view').enabled()
return conversation.enabled()
def comment_transform_message(self): def comment_transform_message(self):
"""Returns the description that shows up above the comment text, """Returns the description that shows up above the comment text,

View File

@ -100,6 +100,15 @@
permission="zope2.View" permission="zope2.View"
/> />
<!-- Conversation view -->
<browser:page
name="conversation_view"
for="Products.CMFCore.interfaces.IContentish"
layer="..interfaces.IDiscussionLayer"
class=".conversation.ConversationView"
permission="zope2.View"
/>
<!-- Comment view --> <!-- Comment view -->
<browser:view <browser:view
name="view" name="view"

View File

@ -0,0 +1,95 @@
from zope.component import queryUtility
from plone.registry.interfaces import IRegistry
from Acquisition import aq_inner
from Acquisition import aq_base
from Products.CMFCore.utils import getToolByName
from Products.CMFCore.interfaces import IFolderish
from Products.CMFPlone.interfaces import IPloneSiteRoot
from Products.CMFPlone.interfaces import INonStructuralFolder
from plone.app.discussion.interfaces import IDiscussionSettings
class ConversationView(object):
def enabled(self):
""" Returns True if discussion is enabled for this conversation.
This method checks five different settings in order to figure out if
discussion is enable on a specific content object:
1) Check if discussion is enabled globally in the plone.app.discussion
registry/control panel.
2) If the current content object is a folder, always return
False, since we don't allow comments on a folder. This
setting is used to allow/ disallow comments for all content
objects inside a folder, not for the folder itself.
3) Check if the allow_discussion boolean flag on the content object is
set. If it is set to True or False, return the value. If it set to
None, try further.
4) Traverse to a folder with allow_discussion set to either True or
False. If allow_discussion is not set (None), traverse further until
we reach the PloneSiteRoot.
5) Check if discussion is allowed for the content type.
"""
context = aq_inner(self.context)
# Fetch discussion registry
registry = queryUtility(IRegistry)
settings = registry.forInterface(IDiscussionSettings, check=False)
# Check if discussion is allowed globally
if not settings.globally_enabled:
return False
# Always return False if object is a folder
if (IFolderish.providedBy(context) and
not INonStructuralFolder.providedBy(context)):
return False
def traverse_parents(context):
# Run through the aq_chain of obj and check if discussion is
# enabled in a parent folder.
for obj in context.aq_chain:
if not IPloneSiteRoot.providedBy(obj):
if (IFolderish.providedBy(obj) and
not INonStructuralFolder.providedBy(obj)):
flag = getattr(obj, 'allow_discussion', None)
if flag is not None:
return flag
return None
# If discussion is disabled for the object, bail out
obj_flag = getattr(aq_base(context), 'allow_discussion', None)
if obj_flag is False:
return False
# Check if traversal returned a folder with discussion_allowed set
# to True or False.
folder_allow_discussion = traverse_parents(context)
if folder_allow_discussion is True:
if not getattr(self, 'allow_discussion', None):
return True
elif folder_allow_discussion is False:
if obj_flag:
return True
# Check if discussion is allowed on the content type
portal_types = getToolByName(self, 'portal_types')
document_fti = getattr(portal_types, context.portal_type)
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 not obj_flag:
return False
return True

View File

@ -14,10 +14,9 @@ import time
from persistent import Persistent from persistent import Persistent
from plone.registry.interfaces import IRegistry
from zope.interface import implements, implementer from zope.interface import implements, implementer
from zope.component import adapts, adapter, queryUtility from zope.component import adapts
from zope.component import adapter
from zope.annotation.interfaces import IAnnotations, IAnnotatable from zope.annotation.interfaces import IAnnotations, IAnnotatable
@ -31,11 +30,6 @@ 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 Products.CMFCore.interfaces import IFolderish
from Products.CMFPlone.interfaces import IPloneSiteRoot, INonStructuralFolder
from zope.container.contained import ContainerModifiedEvent from zope.container.contained import ContainerModifiedEvent
from zope.lifecycleevent import ObjectCreatedEvent from zope.lifecycleevent import ObjectCreatedEvent
@ -49,7 +43,6 @@ from BTrees.LOBTree import LOBTree
from BTrees.LLBTree import LLSet from BTrees.LLBTree import LLSet
from plone.app.discussion.interfaces import IConversation from plone.app.discussion.interfaces import IConversation
from plone.app.discussion.interfaces import IDiscussionSettings
from plone.app.discussion.interfaces import IReplies from plone.app.discussion.interfaces import IReplies
from plone.app.discussion.comment import Comment from plone.app.discussion.comment import Comment
@ -87,63 +80,8 @@ class Conversation(Traversable, Persistent, Explicit):
return self.id return self.id
def enabled(self): def enabled(self):
# Returns True if discussion is enabled on the conversation
# Fetch discussion registry
registry = queryUtility(IRegistry)
settings = registry.forInterface(IDiscussionSettings, check=False)
# Check if discussion is allowed globally
if not settings.globally_enabled:
return False
parent = aq_inner(self.__parent__) parent = aq_inner(self.__parent__)
return parent.restrictedTraverse('@@conversation_view').enabled()
# Always return False if object is a folder
if (IFolderish.providedBy(parent) and
not INonStructuralFolder.providedBy(parent)):
return False
def traverse_parents(obj):
# Run through the aq_chain of obj and check if discussion is
# enabled in a parent folder.
for obj in self.aq_chain:
if not IPloneSiteRoot.providedBy(obj):
if (IFolderish.providedBy(obj) and
not INonStructuralFolder.providedBy(obj)):
flag = getattr(obj, 'allow_discussion', None)
if flag is not None:
return flag
return None
obj = aq_parent(self)
# If discussion is disabled for the object, bail out
obj_flag = getattr(aq_base(obj), 'allow_discussion', None)
if obj_flag is False:
return False
# Check if traversal returned a folder with discussion_allowed set
# to True or False.
folder_allow_discussion = traverse_parents(obj)
if folder_allow_discussion is True:
if not getattr(self, 'allow_discussion', None):
return True
elif folder_allow_discussion is False:
if obj_flag:
return True
# Check if discussion is allowed on the content type
portal_types = getToolByName(self, 'portal_types')
document_fti = getattr(portal_types, obj.portal_type)
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 not obj_flag:
return False
return True
@property @property
def total_comments(self): def total_comments(self):

View File

@ -168,31 +168,6 @@ class IConversation(IIterableMapping):
readonly=True, readonly=True,
) )
def enabled():
""" Returns True if discussion is enabled for this conversation.
This method checks five different settings in order to figure out if
discussion is enable on a specific content object:
1) Check if discussion is enabled globally in the plone.app.discussion
registry/control panel.
2) If the current content object is a folder, always return False, since
we don't allow comments on a folder. This setting is used to allow/
disallow comments for all content objects inside a folder, not for
the folder itself.
3) Check if the allow_discussion boolean flag on the content object is
set. If it is set to True or False, return the value. If it set to
None, try further.
4) Traverse to a folder with allow_discussion set to either True or
False. If allow_discussion is not set (None), traverse further until
we reach the PloneSiteRoot.
5) Check if discussion is allowed for the content type.
"""
def addComment(comment): def addComment(comment):
"""Adds a new comment to the list of comments, and returns the """Adds a new comment to the list of comments, and returns the
comment id that was assigned. The comment_id property on the comment comment id that was assigned. The comment_id property on the comment

View File

@ -7,6 +7,7 @@ from AccessControl import Unauthorized
from OFS.Image import Image from OFS.Image import Image
from zope import interface
from zope.interface import alsoProvides from zope.interface import alsoProvides
from zope.publisher.browser import TestRequest from zope.publisher.browser import TestRequest
from zope.annotation.interfaces import IAttributeAnnotatable from zope.annotation.interfaces import IAttributeAnnotatable
@ -29,6 +30,7 @@ from Products.PloneTestCase.ptc import PloneTestCase
from plone.app.discussion.browser.comments import CommentsViewlet from plone.app.discussion.browser.comments import CommentsViewlet
from plone.app.discussion.browser.comments import CommentForm from plone.app.discussion.browser.comments import CommentForm
from plone.app.discussion import interfaces
from plone.app.discussion.interfaces import IConversation from plone.app.discussion.interfaces import IConversation
from plone.app.discussion.tests.layer import DiscussionLayer from plone.app.discussion.tests.layer import DiscussionLayer
from plone.app.discussion.interfaces import IDiscussionSettings from plone.app.discussion.interfaces import IDiscussionSettings
@ -39,6 +41,9 @@ class TestCommentForm(PloneTestCase):
layer = DiscussionLayer layer = DiscussionLayer
def afterSetUp(self): def afterSetUp(self):
interface.alsoProvides(
self.portal.REQUEST, interfaces.IDiscussionLayer)
self.loginAsPortalOwner() self.loginAsPortalOwner()
typetool = self.portal.portal_types typetool = self.portal.portal_types
typetool.constructContent('Document', self.portal, 'doc1') typetool.constructContent('Document', self.portal, 'doc1')
@ -111,6 +116,8 @@ class TestCommentForm(PloneTestCase):
settings = registry.forInterface(IDiscussionSettings, check=False) settings = registry.forInterface(IDiscussionSettings, check=False)
settings.anonymous_comments = True settings.anonymous_comments = True
self.portal.portal_workflow.doActionFor(self.context, 'publish')
# Logout # Logout
self.logout() self.logout()
@ -211,6 +218,9 @@ class TestCommentsViewlet(PloneTestCase):
layer = DiscussionLayer layer = DiscussionLayer
def afterSetUp(self): def afterSetUp(self):
interface.alsoProvides(
self.portal.REQUEST, interfaces.IDiscussionLayer)
self.loginAsPortalOwner() self.loginAsPortalOwner()
typetool = self.portal.portal_types typetool = self.portal.portal_types
typetool.constructContent('Document', self.portal, 'doc1') typetool.constructContent('Document', self.portal, 'doc1')

View File

@ -1,6 +1,7 @@
import unittest import unittest
from datetime import datetime, timedelta from datetime import datetime, timedelta
from zope import interface
from zope.component import createObject, queryUtility from zope.component import createObject, queryUtility
from zope.annotation.interfaces import IAnnotations from zope.annotation.interfaces import IAnnotations
@ -14,6 +15,7 @@ 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
from plone.app.discussion import interfaces
from plone.app.discussion.interfaces import IConversation from plone.app.discussion.interfaces import IConversation
from plone.app.discussion.interfaces import IComment from plone.app.discussion.interfaces import IComment
from plone.app.discussion.interfaces import IReplies from plone.app.discussion.interfaces import IReplies
@ -25,6 +27,9 @@ class ConversationTest(PloneTestCase):
layer = DiscussionLayer layer = DiscussionLayer
def afterSetUp(self): def afterSetUp(self):
interface.alsoProvides(
self.portal.REQUEST, interfaces.IDiscussionLayer)
# First we need to create some content. # First we need to create some content.
self.loginAsPortalOwner() self.loginAsPortalOwner()
typetool = self.portal.portal_types typetool = self.portal.portal_types
@ -244,7 +249,7 @@ class ConversationTest(PloneTestCase):
self.assertFalse(aq_base(folder).allow_discussion) self.assertFalse(aq_base(folder).allow_discussion)
doc = self.portal.folder1.doc2 doc = self.portal.folder1.doc2
conversation = IConversation(doc) conversation = doc.restrictedTraverse('@@conversation_view')
self.assertEquals(conversation.enabled(), False) self.assertEquals(conversation.enabled(), False)
# We have to allow discussion on Document content type, since # We have to allow discussion on Document content type, since
@ -258,7 +263,8 @@ class ConversationTest(PloneTestCase):
def test_disable_commenting_globally(self): def test_disable_commenting_globally(self):
# Create a conversation. # Create a conversation.
conversation = IConversation(self.portal.doc1) conversation = self.portal.doc1.restrictedTraverse(
'@@conversation_view')
# We have to allow discussion on Document content type, since # We have to allow discussion on Document content type, since
# otherwise allow_discussion will always return False # otherwise allow_discussion will always return False
@ -286,7 +292,7 @@ class ConversationTest(PloneTestCase):
self.typetool.constructContent('News Item', self.portal, 'newsitem') self.typetool.constructContent('News Item', self.portal, 'newsitem')
newsitem = self.portal.newsitem newsitem = self.portal.newsitem
conversation = IConversation(newsitem) conversation = newsitem.restrictedTraverse('@@conversation_view')
# We have to allow discussion on Document content type, since # We have to allow discussion on Document content type, since
# otherwise allow_discussion will always return False # otherwise allow_discussion will always return False
@ -312,7 +318,8 @@ class ConversationTest(PloneTestCase):
def test_disable_commenting_for_content_type(self): def test_disable_commenting_for_content_type(self):
# Create a conversation. # Create a conversation.
conversation = IConversation(self.portal.doc1) conversation = self.portal.doc1.restrictedTraverse(
'@@conversation_view')
# The Document content type is disabled by default # The Document content type is disabled by default
self.assertEquals(conversation.enabled(), False) self.assertEquals(conversation.enabled(), False)
@ -337,11 +344,11 @@ class ConversationTest(PloneTestCase):
# The enabled method should always return False for the folder # The enabled method should always return False for the folder
# itself. # itself.
# Create a folder # Create a folderp
self.typetool.constructContent('Folder', self.portal, 'f1') self.typetool.constructContent('Folder', self.portal, 'f1')
f1 = self.portal.f1 f1 = self.portal.f1
# Usually we don't create a conversation on a folder # Usually we don't create a conversation on a folder
conversation = IConversation(self.portal.f1) conversation = self.portal.f1.restrictedTraverse('@@conversation_view')
# Allow discussion for the folder # Allow discussion for the folder
self.portal_discussion.overrideDiscussionFor(f1, True) self.portal_discussion.overrideDiscussionFor(f1, True)
@ -369,7 +376,7 @@ class ConversationTest(PloneTestCase):
# Create a document inside the folder # Create a document inside the folder
self.typetool.constructContent('Document', f1, 'doc1') self.typetool.constructContent('Document', f1, 'doc1')
doc1 = self.portal.f1.doc1 doc1 = self.portal.f1.doc1
doc1_conversation = IConversation(doc1) doc1_conversation = doc1.restrictedTraverse('@@conversation_view')
self.assertEquals(doc1_conversation.enabled(), False) self.assertEquals(doc1_conversation.enabled(), False)
@ -389,7 +396,8 @@ class ConversationTest(PloneTestCase):
# Allow discussion on a single content object # Allow discussion on a single content object
# Create a conversation. # Create a conversation.
conversation = IConversation(self.portal.doc1) conversation = self.portal.doc1.restrictedTraverse(
'@@conversation_view')
# Discussion is disallowed by default # Discussion is disallowed by default
self.assertEquals(conversation.enabled(), False) self.assertEquals(conversation.enabled(), False)