[CMF-checkins] CVS: CMF - DiscussionItem.py:1.15 DiscussionTool.py:1.3 File.py:1.14 Image.py:1.10 Link.py:1.6 NewsItem.py:1.9 Portal.py:1.22 URLTool.py:1.6 __init__.py:1.9

tseaver@digicool.com tseaver@digicool.com
Sun, 17 Jun 2001 15:21:37 -0400 (EDT)


Update of /cvs-repository/CMF/CMFDefault
In directory korak.digicool.com:/tmp/cvs-serv28068/CMFDefault

Modified Files:
	DiscussionItem.py DiscussionTool.py File.py Image.py Link.py 
	NewsItem.py Portal.py URLTool.py __init__.py 
Log Message:
 - Merge 'discussiongeddon' patch.


--- Updated File DiscussionItem.py in package CMF --
--- DiscussionItem.py	2001/06/08 15:07:22	1.14
+++ DiscussionItem.py	2001/06/17 19:21:05	1.15
@@ -83,17 +83,44 @@
 # 
 ##############################################################################
 
-import Globals
-from Globals import HTMLFile, Persistent, PersistentMapping
-from Acquisition import Implicit, aq_base
-from Discussions import DiscussionResponse
-from Document import Document
-from DublinCore import DefaultDublinCoreImpl
+import urllib, string
+from Globals import HTMLFile, Persistent, PersistentMapping, InitializeClass
+from AccessControl import ClassSecurityInfo
+from Acquisition import Implicit, aq_base, aq_inner, aq_parent
+from OFS.Traversable import Traversable
 from DateTime import DateTime
+
+from Products.CMFCore import CMFCorePermissions
 from Products.CMFCore.utils import getToolByName
 from Products.CMFCore.PortalContent import PortalContent
-import urllib, string
 
+from Document import Document
+from DublinCore import DefaultDublinCoreImpl
+
+
+factory_type_information = ( { 'id'             : 'Discussion Item'
+                             , 'meta_type'      : 'Discussion Item'
+                             , 'description'    : """\
+Discussion Items are documents which reply to other content.
+They should *not* be addable through the standard 'folder_factories'
+interface."""
+                             , 'icon'           : 'discussionitem_icon.gif'
+                             , 'product'        : '' # leave blank to suppress
+                             , 'factory'        : ''
+                             , 'immediate_view' : ''
+                             , 'actions'        :
+                                ( { 'name'          : 'View'
+                                  , 'action'        : 'discussionitem_view'
+                                  , 'permissions'   : (
+                                      CMFCorePermissions.View, )
+                                  }
+                                ,
+                                )
+                             }
+                           ,
+                           )
+
+
 def addDiscussionItem(self, id, title, description, text_format, text,
                       reply_to, RESPONSE=None):
     """
@@ -123,131 +150,104 @@
 
     
 class DiscussionItem( Document
-                    , DiscussionResponse
                     , DefaultDublinCoreImpl
                     ):
     """
-    This is the PortalContent object for content which is a response to other
-    content.
+        Class for content which is a response to other content.
     """
-    meta_type = 'Discussion Item'
-    allow_discussion = 1
-    creator = 'unknown'
-
-    __ac_permissions__ = (
-        ('Change Discussion Items', ('edit',), ('Owner',)),
-        ('View', ('', 'absolute_url', 'getReplies', 'view')),
-        )
-
-    view = HTMLFile('dtml/discussionView',globals())
-    index_html = view
-    # Ensure the name of the "view" method isn't ambiguous.
-    view.__name__ = 'view'
-    view._need__name__ = 0
-
-    editForm = HTMLFile('dtml/discussionEdit',globals())
-
-    # Replies should default to published
-    review_state='published'
-
-    def absolute_url(self, relative=0):
-        portal_url = getToolByName(self, 'portal_url')
-        container = self.aq_inner.aq_parent
-        content_item = container.aq_inner.aq_parent
-        parent_rel_url = portal_url.getRelativeUrl(content_item)
+    meta_type           = 'Discussion Item'
+    allow_discussion    = 1
+    creator             = 'unknown'
+    in_reply_to         = None
+    review_state        ='published'
 
-        fmt_string = '%s/%s/talkback/%s'
+    security = ClassSecurityInfo()
 
-        if relative:
-            prefix = portal_url.getPortalPath()
-        else:
-            prefix = portal_url()
-
-        return fmt_string % ( prefix, parent_rel_url, str( self.getId() ) )
-
-    def getPhysicalPath(self):
+    security.declareProtected( CMFCorePermissions.View, 'Creator' )
+    def Creator( self ):
         """
-        Needs to be overridden here because the standard implementation
-        doesn't fit my needs in case i am stored in a DiscussionItemContainer
+            We need to return user who replied, rather than executable
+            owner.
         """
-        return tuple(string.split(self.absolute_url(1), '/'))
+        #   XXX:  revisit if Creator becomes "real" attribute for stock DC.
+        return self.creator
+    
+    #
+    #   DiscussionResponse interface
+    #
+    security.declareProtected( CMFCorePermissions.View, 'inReplyTo' )
+    def inReplyTo( self, REQUEST=None ):
+        """
+            Return the Discussable object to which we are a reply.
+
+            Two cases obtain:
+
+              - We are a "top-level" reply to a non-DiscussionItem piece
+                of content;  in this case, our 'in_reply_to' field will
+                be None.
+            
+              - We are a nested reply;  in this case, our 'in_reply_to'
+                field will be the ID of the parent DiscussionItem.
+        """
+        tool = getToolByName( self, 'portal_discussion' )
+        talkback = tool.getDiscussionFor( self )
+        return talkback._getReplyParent( self.in_reply_to )
 
-    def getReplies(self):
+    security.declarePrivate( CMFCorePermissions.View, 'setReplyTo' )
+    def setReplyTo( self, reply_to ):
         """
-        Return a list of all objects that have their "in_reply_to"
-        attribute set to my own URL
-        """
-        result = []
-        my_url = urllib.unquote( self.absolute_url(1) )
-        wf = getToolByName( self, 'portal_workflow' )
-        talkback = self.aq_inner.aq_parent
-
-        for item in talkback._container.values():
-            if item.in_reply_to == my_url:
-                #if wf.getInfoFor( item, 'review_state' ) == 'published':
-                    result.append(item.__of__(talkback))
-
-        return result
-
-    def __call__(self, REQUEST, **kw):
-        return apply(self.view, (self, REQUEST), kw)
-
-    def edit(self, text_format, text, file='', REQUEST=None):
+            Make this object a response to the passed object.
         """
-        Edit the discussion item.
+        if getattr( reply_to, 'meta_type', None ) == self.meta_type:
+            self.in_reply_to = reply_to.getId()
+        else:
+            self.in_reply_to = None
+    
+    security.declareProtected( CMFCorePermissions.View, 'parentsInThread' )
+    def parentsInThread( self, size=0 ):
         """
+            Return the list of items which are "above" this item in
+            the discussion thread.
 
-        Document.edit(self, text_format, text, file)
-        if REQUEST is not None:
-            return self.editForm(self, REQUEST, portal_status_message= \
-                                 'Discussion item changed.')
-
-    def Creator( self ):
+            If 'size' is not zero, only the closest 'size' parents
+            will be returned.
         """
-        """
-        return self.creator
-
+        parents = []
+        current = self
+        while not size or len( parents ) < size:
+            parent = current.inReplyTo()
+            assert not parent in parents  # sanity check
+            parents.insert( 0, parent )
+            if parent.meta_type != self.meta_type:
+                break
+            current = parent
+        return parents
 
-Globals.default__class_init__(DiscussionItem)
+InitializeClass( DiscussionItem )
 
-
-class DiscussionItemContainer(Persistent, Implicit):
+class DiscussionItemContainer( Persistent, Implicit, Traversable ):
     """
-    This class stores DiscussionItem objects. Discussable 
-    content that has DiscussionItems associated with it 
-    will have an instance of DiscussionItemContainer 
-    injected into it to hold the discussion threads.
+        Store DiscussionItem objects. Discussable content that
+        has DiscussionItems associated with it will have an
+        instance of DiscussionItemContainer injected into it to
+        hold the discussion threads.
     """
 
     # for the security machinery to allow traversal
-    __roles__ = None
-    __allow_access_to_unprotected_subobjects__ = 1   # legacy code
-
+    #__roles__ = None
 
-    __ac_permissions__ = ( ( 'Access contents information'
-                           , ( 'objectIds'
-                             , 'objectValues'
-                             , 'objectItems'
-                             )
-                           )
-                         , ( 'View'
-                           , ( 'hasReplies'
-                             , 'getReplies'
-                             , '__bobo_traverse__'
-                             )
-                           )
-                         , ( 'Reply to item'
-                           , ( 'createReply'
-                             ,
-                             )
-                           )
-                         ) 
+    security = ClassSecurityInfo()
 
     def __init__(self):
         self.id = 'talkback'
         self._container = PersistentMapping()
 
+    security.declareProtected( CMFCorePermissions.View, 'getId' )
+    def getId( self ):
+        return self.id
 
+    # Is this right?
+    security.declareProtected( CMFCorePermissions.View, '__bobo_traverse__' )
     def __bobo_traverse__(self, REQUEST, name):
         """
         This will make this container traversable
@@ -261,103 +261,106 @@
             except:
                 REQUEST.RESPONSE.notFoundError("%s\n%s" % (name, ''))
 
+    security.declarePrivate( 'manage_beforeDelete' )
     def manage_beforeDelete(self, item, container):
-        "Remove the contained items from the catalog."
+        """
+            Remove the contained items from the catalog.
+        """
         if aq_base(container) is not aq_base(self):
-            for obj in self.getReplies():
-                obj.manage_beforeDelete(item, container)
+            for obj in self.objectValues():
+                obj.__of__( self ).manage_beforeDelete( item, container )
 
-    def objectIds(self, spec=None):
+    #
+    #   OFS.ObjectManager query interface.
+    #
+    security.declareProtected( CMFCorePermissions.AccessContentsInformation
+                             , 'objectIds' )
+    def objectIds( self, spec=None ):
         """
-        return a list of ids of DiscussionItems in
-        this DiscussionItemContainer
+            Return a list of the ids of our DiscussionItems.
         """
+        if spec and spec is not DiscussionItem.meta_type:
+            return []
         return self._container.keys()
 
+
+    security.declareProtected( CMFCorePermissions.AccessContentsInformation
+                             , 'objectItems' )
     def objectItems(self, spec=None):
         """
-        Returns a list of (id, subobject) tuples of the current object.
-        If 'spec' is specified, returns only objects whose meta_type
-        match 'spec'
+            Return a list of (id, subobject) tuples for our DiscussionItems.
         """
         r=[]
         a=r.append
         g=self._container.get
-        for id in self.objectIds(spec): a((id, g(id)))
+        for id in self.objectIds(spec):
+            a( (id, g( id ) ) )
         return r
 
+
+    security.declareProtected( CMFCorePermissions.AccessContentsInformation
+                             , 'objectValues' )
     def objectValues(self):
         """
-        return the list of objects stored in this
-        DiscussionItemContainer
+            Return a list of our DiscussionItems.
         """
         return self._container.values()
 
-    def createReply(self, title, text, REQUEST={}, RESPONSE=None):
+    #
+    #   Discussable interface
+    #
+    security.declareProtected( CMFCorePermissions.ReplyToItem, 'createReply' )
+    def createReply( self, title, text, Creator=None ):
         """
             Create a reply in the proper place
         """
         container = self._container
 
         id = int(DateTime().timeTime())
-        while getattr(self._container, `id`, None) is not None:
+        while self._container.get( str(id), None ) is not None:
             id = id + 1
+        id = str( id )
+
+        item = DiscussionItem( id, title=title, description=title )
+        item._edit( text_format='structured-text', text=text )
 
-        item = DiscussionItem( `id` )
-        item.title = title
-        item.description = title
-        item._edit('structured-text', text)
+        if Creator:
+            item.creator = Creator
 
-        if REQUEST.has_key( 'Creator' ):
-            item.creator = REQUEST[ 'Creator' ]
+        item.__of__( self ).indexObject()
 
-        item.__of__(self).setReplyTo(self.aq_parent)
+        item.setReplyTo( self._getDiscussable() )
  
-        self._container[`id`] = item
+        self._container[ id ] = item
 
-        if RESPONSE is not None:
-            RESPONSE.redirect( self.aq_inner.aq_parent.absolute_url() + '/view' )
+        return id
 
-    def hasReplies(self):
+    security.declareProtected( CMFCorePermissions.View, 'hasReplies' )
+    def hasReplies( self ):
         """
-        Test to see if there are any dicussion items
+            Test to see if there are any dicussion items
         """
-        if len(self._container) > 0:
-            return 1
-        else:
+        if len(self._container) == 0:
             return 0
-
-    def _getReplyResults(self):
-        """
-           Get a list of ids within the discussion item container that are 
-           in reply to me
-        """
-        result = []
-        portal_url = getToolByName(self, 'portal_url')
-        my_url = urllib.unquote( portal_url.getRelativeUrl( self ) )
-        wf = getToolByName( self, 'portal_workflow' )
-
-        for item in self._container.values():
-            if item.in_reply_to == my_url:
-                #if wf.getInfoFor( item, 'review_state' ) == 'published':
-                    result.append(item.getId())
 
-        return result
+        return len( self._getReplyResults() )
 
-    def getReplies(self):
+    security.declareProtected( CMFCorePermissions.View, 'getReplies' )
+    def getReplies( self ):
         """
             Return a sequence of the DiscussionResponse objects which are
             associated with this Discussable
         """
         objects = []
+        a = objects.append
         result_ids = self._getReplyResults()
 
         for id in result_ids:
-            objects.append(self._container.get(id).__of__(self))
+            a( self._container.get( id ).__of__( self ) )
 
         return objects
 
-
+    security.declareProtected( CMFCorePermissions.View, 'quotedContents' )
     def quotedContents(self):
         """
             Return this object's contents in a form suitable for inclusion
@@ -365,7 +368,52 @@
         """
  
         return ""
+    
+    #
+    #   Utility methods
+    #
+    security.declarePrivate( '_getReplyParent' )
+    def _getReplyParent( self, in_reply_to ):
+        """
+            Return the object indicated by the 'in_reply_to', where
+            'None' represents the "outer" content object.
+        """
+        outer = self._getDiscussable( outer=1 )
+        if in_reply_to is None:
+            return outer
+        parent = self._container[ in_reply_to ].__of__( aq_inner( self ) )
+        return parent.__of__( outer )
+        
+
+    security.declarePrivate( '_getDiscussable' )
+    def _getDiscussable( self, outer=0 ):
+        """
+        """
+        tb = outer and aq_inner( self ) or self
+        return getattr( tb, 'aq_parent', None )
+
+    security.declarePrivate( '_getReplyResults' )
+    def _getReplyResults( self ):
+        """
+           Get a list of ids of DiscussionItems which are replies to
+           our Discussable.
+        """
+        discussable = self._getDiscussable()
+        outer = self._getDiscussable( outer=1 )
+
+        if discussable == outer:
+            in_reply_to = None
+        else:
+            in_reply_to = discussable.getId()
+
+        result = []
+        a = result.append
+        for key, value in self._container.items():
+            if value.in_reply_to == in_reply_to:
+                a( key )
+
+        return result
 
-Globals.default__class_init__(DiscussionItemContainer)
+InitializeClass( DiscussionItemContainer )
 
 

--- Updated File DiscussionTool.py in package CMF --
--- DiscussionTool.py	2001/05/11 03:41:43	1.2
+++ DiscussionTool.py	2001/06/17 19:21:05	1.3
@@ -89,20 +89,30 @@
 __version__='$Revision$'[11:-2]
 
 
-from DiscussionItem import DiscussionItemContainer
-from Products.CMFCore.DiscussionTool import DiscussionTool
-
 from Globals import InitializeClass, DTMLFile
 from AccessControl import ClassSecurityInfo
+from OFS.SimpleItem import SimpleItem
+
+from Products.CMFCore.utils import UniqueObject, getToolByName
 from Products.CMFCore import CMFCorePermissions
+
 from utils import _dtmldir
+from DiscussionItem import DiscussionItemContainer
 
-class DiscussionTool (DiscussionTool):
+class DiscussionNotAllowed( Exception ):
+    pass
+
+class DiscussionTool( UniqueObject, SimpleItem ):
+
     id = 'portal_discussion'
     meta_type = 'Default Discussion Tool'
 
     security = ClassSecurityInfo()
 
+    manage_options = ( { 'label' : 'Overview', 'action' : 'manage_overview' }
+                     , 
+                     ) + SimpleItem.manage_options
+
     #
     #   ZMI methods
     #
@@ -113,12 +123,70 @@
     #
     #   'portal_discussion' interface methods
     #
-    security.declarePublic( 'createDiscussionFor' )
-    def createDiscussionFor(self, object):
+
+    security.declarePublic( 'getDiscussionFor' )
+    def getDiscussionFor(self, content):
         """
-        This method will create the object that holds 
-        discussion items inside the object being discussed.
+            Return the talkback for content, creating it if need be.
         """
-        object.talkback = DiscussionItemContainer()
+        if not self.isDiscussionAllowedFor( content ):
+            raise DiscussionNotAllowed
+            
+        talkback = getattr( content, 'talkback', None )
+        if not talkback:
+            talkback = self._createDiscussionFor( content )
+        
+        return talkback
+
+    security.declarePublic( 'isDiscussionAllowedFor' )
+    def isDiscussionAllowedFor( self, content ):
+        '''
+            Returns a boolean indicating whether a discussion is
+            allowed for the specified content.
+        '''
+        if hasattr( content, 'allow_discussion' ):
+            return content.allow_discussion
+        typeInfo = getToolByName(self, 'portal_types').getTypeInfo( content )
+        if typeInfo:
+            return typeInfo.allowDiscussion()
+        return 0
+
+    #
+    #   ActionProvider interface
+    #
+    security.declarePrivate( 'listActions' )
+    def listActions(self, info):
+        # Return actions for reply and show replies
+        content = info.content
+        if content is None or not self.isDiscussionAllowedFor(content):
+            return None
+
+        discussion = self.getDiscussionFor(content)
+        discussion_url = info.content_url
+
+        actions = (
+            {'name': 'Reply',
+             'url': discussion_url + '/discussion_reply_form',
+             'permissions': ['Reply to item'],
+             'category': 'object'
+             },
+            )
+
+        return actions
+
+    #
+    #   Utility methods
+    #
+    security.declarePrivate( '_createDiscussionFor' )
+    def _createDiscussionFor( self, content ):
+        """
+            Create the object that holds discussion items inside
+            the object being discussed, if allowed.
+        """
+        if not self.isDiscussionAllowedFor( content ):
+            raise DiscussionNotAllowed
+
+        content.talkback = DiscussionItemContainer()
+        return content.talkback
 
-InitializeClass(DiscussionTool)
+InitializeClass( DiscussionTool )

--- Updated File File.py in package CMF --
--- File.py	2001/06/13 00:20:56	1.13
+++ File.py	2001/06/17 19:21:05	1.14
@@ -94,7 +94,6 @@
 
 from Globals import HTMLFile, HTML
 from Products.CMFCore.PortalContent import PortalContent
-from Discussions import Discussable
 import Globals
 from DublinCore import DefaultDublinCoreImpl
 

--- Updated File Image.py in package CMF --
--- Image.py	2001/06/13 00:20:56	1.9
+++ Image.py	2001/06/17 19:21:05	1.10
@@ -93,7 +93,6 @@
 
 from Globals import HTMLFile, HTML
 from Products.CMFCore.PortalContent import PortalContent
-from Discussions import Discussable
 import Globals
 from DublinCore import DefaultDublinCoreImpl
 

--- Updated File Link.py in package CMF --
--- Link.py	2001/05/24 20:39:40	1.5
+++ Link.py	2001/06/17 19:21:05	1.6
@@ -89,7 +89,6 @@
 
 import Globals
 from Globals import HTMLFile, HTML
-from Discussions import Discussable
 from Products.CMFCore.PortalContent import PortalContent
 from DublinCore import DefaultDublinCoreImpl
 

--- Updated File NewsItem.py in package CMF --
--- NewsItem.py	2001/05/25 17:44:51	1.8
+++ NewsItem.py	2001/06/17 19:21:05	1.9
@@ -90,7 +90,6 @@
  
 import Globals
 from Globals import HTMLFile, HTML
-from Discussions import Discussable
 from Document import Document
 from utils import parseHeadersBody
 

--- Updated File Portal.py in package CMF --
--- Portal.py	2001/06/14 15:41:42	1.21
+++ Portal.py	2001/06/17 19:21:05	1.22
@@ -91,7 +91,7 @@
 from Products.CMFTopic import Topic, topic_globals
 from DublinCore import DefaultDublinCoreImpl
 
-import Document, Image, File, Link, NewsItem, Favorite
+import Document, Image, File, Link, NewsItem, Favorite, DiscussionItem
 
 factory_type_information = ( Document.factory_type_information
                            + Image.factory_type_information
@@ -99,6 +99,7 @@
                            + Link.factory_type_information
                            + NewsItem.factory_type_information
                            + Favorite.factory_type_information
+                           + DiscussionItem.factory_type_information
                            )
 
 

--- Updated File URLTool.py in package CMF --
--- URLTool.py	2001/05/11 03:41:43	1.5
+++ URLTool.py	2001/06/17 19:21:05	1.6
@@ -135,6 +135,24 @@
         """
         return self.aq_inner.aq_parent
 
+    security.declarePublic( 'getRelativeContentPath' )
+    def getRelativeContentPath( self, content ):
+        """
+            Return the path (sequence of IDs) for an object, relative 
+            to the portal root
+        """
+        portal_path_length = len(self.aq_inner.aq_parent.getPhysicalPath())
+        content_location = content.getPhysicalPath()
+        return content_location[portal_path_length:]
+
+    security.declarePublic( 'getRelativeContentURL' )
+    def getRelativeContentURL( self, content ):
+        """
+            Return the URL (slash-separated string) for an object,
+            relative to the portal root
+        """
+        return string.join( self.getRelativeContentPath( content ), '/' )
+
     security.declarePublic( 'getRelativeUrl' )
     def getRelativeUrl(self, content):
         """

--- Updated File __init__.py in package CMF --
--- __init__.py	2001/06/01 02:40:37	1.8
+++ __init__.py	2001/06/17 19:21:05	1.9
@@ -95,7 +95,7 @@
 except ImportError:
     HAS_SKINNED_FOLDER=0
 
-import Discussions, DiscussionItem
+import DiscussionItem
 import PropertiesTool, MembershipTool, MetadataTool
 import RegistrationTool, URLTool, DublinCore, DiscussionTool
 import SyndicationTool
@@ -123,7 +123,6 @@
                          , ( 'Products.PTKBase.Image', Image )
                          , ( 'Products.PTKBase.Link', Link )
                          , ( 'Products.PTKBase.NewsItem', NewsItem )
-                         , ( 'Products.PTKBase.Discussions', Discussions )
                          )
 
     #   ...and make sure we can find them in PTKBase when we do
@@ -146,17 +145,16 @@
     contentClasses = contentClasses + ( SkinnedFolder.SkinnedFolder, )
 
 contentConstructors = ( Document.addDocument
-                        , File.addFile
-                        , Image.addImage
-                        , Link.addLink
-                        , Favorite.addFavorite
-                        , NewsItem.addNewsItem
-                        )
+                      , File.addFile
+                      , Image.addImage
+                      , Link.addLink
+                      , Favorite.addFavorite
+                      , NewsItem.addNewsItem
+                      )
 
 bases = ( ( Portal.CMFSite
-          , Discussions.Discussable
-          , Discussions.DiscussionResponse
           , DublinCore.DefaultDublinCoreImpl
+          , DiscussionItem.DiscussionItem
           )
           + contentClasses
         )