Back to index

plone3  3.1.7
DiscussionItem.py
Go to the documentation of this file.
00001 ##############################################################################
00002 #
00003 # Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
00004 #
00005 # This software is subject to the provisions of the Zope Public License,
00006 # Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
00007 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
00008 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
00009 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
00010 # FOR A PARTICULAR PURPOSE.
00011 #
00012 ##############################################################################
00013 """ Discussion item portal type.
00014 
00015 $Id: DiscussionItem.py 77356 2007-07-03 14:55:11Z yuppie $
00016 """
00017 
00018 from AccessControl import ClassSecurityInfo
00019 from Acquisition import Implicit, aq_base, aq_inner, aq_parent
00020 from DateTime import DateTime
00021 from Globals import InitializeClass
00022 from Globals import Persistent
00023 from Globals import PersistentMapping
00024 from OFS.Traversable import Traversable
00025 from zope.component import getUtility
00026 from zope.interface import implements
00027 
00028 from Products.CMFCore.interfaces import ICallableOpaqueItemEvents
00029 from Products.CMFCore.interfaces import IDiscussable
00030 from Products.CMFCore.interfaces import IDiscussionResponse
00031 from Products.CMFCore.interfaces import IDiscussionTool
00032 from Products.CMFCore.interfaces.Discussions \
00033         import Discussable as z2IDiscussable
00034 from Products.CMFCore.interfaces.Discussions \
00035         import DiscussionResponse as z2IDiscussionResponse
00036 
00037 from Document import Document
00038 from permissions import AccessContentsInformation
00039 from permissions import ManagePortal
00040 from permissions import ReplyToItem
00041 from permissions import View
00042 from utils import scrubHTML
00043 
00044 
00045 def addDiscussionItem(self, id, title, description, text_format, text,
00046                       reply_to, RESPONSE=None):
00047     """ Add a discussion item
00048 
00049     'title' is also used as the subject header
00050     if 'description' is blank, it is filled with the contents of 'title'
00051     'reply_to' is the object (or path to the object) which this is a reply to
00052 
00053     Otherwise, same as addDocument
00054     """
00055 
00056     if not description: description = title
00057     text = scrubHTML(text)
00058     item = DiscussionItem( id )
00059     item.title = title
00060     item.description = description
00061     item.text_format = text_format
00062     item.text = text
00063     item.setReplyTo(reply_to)
00064 
00065     item._parse()
00066     self._setObject(id, item)
00067 
00068     if RESPONSE is not None:
00069         RESPONSE.redirect(self.absolute_url())
00070 
00071 
00072 class DiscussionItem(Document):
00073 
00074     """ Class for content which is a response to other content.
00075     """
00076 
00077     implements(IDiscussionResponse)
00078     __implements__ = (z2IDiscussionResponse, Document.__implements__)
00079 
00080     meta_type           = 'Discussion Item'
00081     portal_type         = 'Discussion Item'
00082     allow_discussion    = 1
00083     in_reply_to         = None
00084     # XXX this is wrong, it precludes the use of a normal workflow.
00085     review_state        ='published'
00086 
00087     security = ClassSecurityInfo()
00088 
00089     security.declareProtected(View, 'listCreators')
00090     def listCreators(self):
00091         """ List Dublin Core Creator elements - resource authors.
00092         """
00093         if not hasattr(aq_base(self), 'creators'):
00094             # for content created with CMF versions before 1.5
00095             if hasattr(aq_base(self), 'creator') and self.creator != 'unknown':
00096                 self.creators = ( self.creator, )
00097             else:
00098                 self.creators = ()
00099         return self.creators
00100 
00101     #
00102     #   IDiscussionResponse interface
00103     #
00104     security.declareProtected(View, 'inReplyTo')
00105     def inReplyTo( self, REQUEST=None ):
00106         """ Return the IDiscussable object to which we are a reply.
00107 
00108             Two cases obtain:
00109 
00110               - We are a "top-level" reply to a non-DiscussionItem piece
00111                 of content;  in this case, our 'in_reply_to' field will
00112                 be None.
00113 
00114               - We are a nested reply;  in this case, our 'in_reply_to'
00115                 field will be the ID of the parent DiscussionItem.
00116         """
00117         tool = getUtility(IDiscussionTool)
00118         talkback = tool.getDiscussionFor( self )
00119         return talkback._getReplyParent( self.in_reply_to )
00120 
00121     security.declarePrivate(View, 'setReplyTo')
00122     def setReplyTo( self, reply_to ):
00123         """
00124             Make this object a response to the passed object.
00125         """
00126         if getattr( reply_to, 'meta_type', None ) == self.meta_type:
00127             self.in_reply_to = reply_to.getId()
00128         else:
00129             self.in_reply_to = None
00130 
00131     security.declareProtected(View, 'parentsInThread')
00132     def parentsInThread( self, size=0 ):
00133         """
00134             Return the list of items which are "above" this item in
00135             the discussion thread.
00136 
00137             If 'size' is not zero, only the closest 'size' parents
00138             will be returned.
00139         """
00140         parents = []
00141         current = self
00142         while not size or len( parents ) < size:
00143             parent = current.inReplyTo()
00144             assert not parent in parents  # sanity check
00145             parents.insert( 0, parent )
00146             if parent.meta_type != self.meta_type:
00147                 break
00148             current = parent
00149         return parents
00150 
00151 InitializeClass( DiscussionItem )
00152 
00153 
00154 class DiscussionItemContainer( Persistent, Implicit, Traversable ):
00155 
00156     """
00157         Store DiscussionItem objects. Discussable content that
00158         has DiscussionItems associated with it will have an
00159         instance of DiscussionItemContainer injected into it to
00160         hold the discussion threads.
00161     """
00162 
00163     implements(IDiscussable, ICallableOpaqueItemEvents)
00164     __implements__ = z2IDiscussable
00165 
00166     # for the security machinery to allow traversal
00167     #__roles__ = None
00168 
00169     security = ClassSecurityInfo()
00170 
00171     def __init__(self):
00172         self.id = 'talkback'
00173         self._container = PersistentMapping()
00174 
00175     security.declareProtected(View, 'getId')
00176     def getId( self ):
00177         return self.id
00178 
00179     security.declareProtected(View, 'getReply')
00180     def getReply( self, reply_id ):
00181         """
00182             Return a discussion item, given its ID;  raise KeyError
00183             if not found.
00184         """
00185         return self._container.get( reply_id ).__of__(self)
00186 
00187     # Is this right?
00188     security.declareProtected(View, '__bobo_traverse__')
00189     def __bobo_traverse__(self, REQUEST, name):
00190         """
00191         This will make this container traversable
00192         """
00193         target = getattr(self, name, None)
00194         if target is not None:
00195             return target
00196 
00197         else:
00198             try:
00199                 return self.getReply(name)
00200             except:
00201                 parent = aq_parent( aq_inner( self ) )
00202                 if parent.getId() == name:
00203                     return parent
00204                 else:
00205                     REQUEST.RESPONSE.notFoundError("%s\n%s" % (name, ''))
00206 
00207     security.declarePrivate('manage_afterAdd')
00208     def manage_afterAdd(self, item, container):
00209         """
00210             We have juste been added or moved.
00211             Add the contained items to the catalog.
00212         """
00213         if aq_base(container) is not aq_base(self):
00214             for obj in self.objectValues():
00215                 obj.__of__(self).indexObject()
00216 
00217     security.declarePrivate('manage_afterClone')
00218     def manage_afterClone(self, item):
00219         """
00220             We have just been cloned.
00221             Notify the workflow about the contained items.
00222         """
00223         for obj in self.objectValues():
00224             obj.__of__(self).notifyWorkflowCreated()
00225 
00226     security.declarePrivate( 'manage_beforeDelete' )
00227     def manage_beforeDelete(self, item, container):
00228         """
00229             Remove the contained items from the catalog.
00230         """
00231         if aq_base(container) is not aq_base(self):
00232             for obj in self.objectValues():
00233                 obj.__of__(self).unindexObject()
00234 
00235     #
00236     #   OFS.ObjectManager query interface.
00237     #
00238     security.declareProtected(AccessContentsInformation, 'objectIds')
00239     def objectIds( self, spec=None ):
00240         """
00241             Return a list of the ids of our DiscussionItems.
00242         """
00243         if spec and spec is not DiscussionItem.meta_type:
00244             return []
00245         return self._container.keys()
00246 
00247 
00248     security.declareProtected(AccessContentsInformation, 'objectItems')
00249     def objectItems(self, spec=None):
00250         """
00251             Return a list of (id, subobject) tuples for our DiscussionItems.
00252         """
00253         r=[]
00254         a=r.append
00255         g=self._container.get
00256         for id in self.objectIds(spec):
00257             a( (id, g( id ) ) )
00258         return r
00259 
00260 
00261     security.declareProtected(AccessContentsInformation, 'objectValues')
00262     def objectValues(self):
00263         """
00264             Return a list of our DiscussionItems.
00265         """
00266         return self._container.values()
00267 
00268     #
00269     #   IDiscussable interface
00270     #
00271     security.declareProtected(ReplyToItem, 'createReply')
00272     def createReply( self, title, text, Creator=None, text_format='structured-text' ):
00273         """
00274             Create a reply in the proper place
00275         """
00276         container = self._container
00277 
00278         id = int(DateTime().timeTime())
00279         while self._container.get( str(id), None ) is not None:
00280             id = id + 1
00281         id = str( id )
00282 
00283         item = DiscussionItem( id, title=title, description=title )
00284         self._container[id] = item
00285         item = item.__of__(self)
00286 
00287         item.setFormat(text_format)
00288         item._edit(text)
00289         item.addCreator(Creator)
00290         item.indexObject()
00291 
00292         item.setReplyTo( self._getDiscussable() )
00293         item.notifyWorkflowCreated()
00294 
00295         return id
00296 
00297     security.declareProtected(ManagePortal, 'deleteReply')
00298     def deleteReply( self, reply_id ):
00299         """ Remove a reply from this container """
00300         if self._container.has_key( reply_id ):
00301             reply = self._container.get( reply_id ).__of__( self )
00302             my_replies = reply.talkback.getReplies()
00303             for my_reply in my_replies:
00304                 my_reply_id = my_reply.getId()
00305                 self.deleteReply(my_reply_id)
00306 
00307             if hasattr( reply, 'unindexObject' ):
00308                 reply.unindexObject()
00309 
00310             del self._container[reply_id]
00311 
00312 
00313     security.declareProtected(View, 'hasReplies')
00314     def hasReplies( self, content_obj ):
00315         """
00316             Test to see if there are any dicussion items
00317         """
00318         outer = self._getDiscussable( outer=1 )
00319         if content_obj == outer:
00320             return bool( len(self._container) )
00321         else:
00322             return bool( len( content_obj.talkback._getReplyResults() ) )
00323 
00324     security.declareProtected(View, 'replyCount')
00325     def replyCount( self, content_obj ):
00326         """ How many replies do i have? """
00327         outer = self._getDiscussable( outer=1 )
00328         if content_obj == outer:
00329             return len( self._container )
00330         else:
00331             replies = content_obj.talkback.getReplies()
00332             return self._repcount( replies )
00333 
00334     security.declarePrivate('_repcount')
00335     def _repcount( self, replies ):
00336         """  counts the total number of replies by recursing thru the various levels
00337         """
00338         count = 0
00339 
00340         for reply in replies:
00341             count = count + 1
00342 
00343             #if there is at least one reply to this reply
00344             replies = reply.talkback.getReplies()
00345             if replies:
00346                 count = count + self._repcount( replies )
00347 
00348         return count
00349 
00350     security.declareProtected(View, 'getReplies')
00351     def getReplies( self ):
00352         """ Return a sequence of the IDiscussionResponse objects which are
00353             associated with this Discussable
00354         """
00355         objects = []
00356         a = objects.append
00357         result_ids = self._getReplyResults()
00358 
00359         for id in result_ids:
00360             a( self._container.get( id ).__of__( self ) )
00361 
00362         return objects
00363 
00364     security.declareProtected(View, 'quotedContents')
00365     def quotedContents(self):
00366         """
00367             Return this object's contents in a form suitable for inclusion
00368             as a quote in a response.
00369         """
00370 
00371         return ""
00372 
00373     #
00374     #   Utility methods
00375     #
00376     security.declarePrivate( '_getReplyParent' )
00377     def _getReplyParent( self, in_reply_to ):
00378         """
00379             Return the object indicated by the 'in_reply_to', where
00380             'None' represents the "outer" content object.
00381         """
00382         outer = self._getDiscussable( outer=1 )
00383         if in_reply_to is None:
00384             return outer
00385         parent = self._container[ in_reply_to ].__of__( aq_inner( self ) )
00386         return parent.__of__( outer )
00387 
00388     security.declarePrivate( '_getDiscussable' )
00389     def _getDiscussable( self, outer=0 ):
00390         """
00391         """
00392         tb = outer and aq_inner( self ) or self
00393         return getattr( tb, 'aq_parent', None )
00394 
00395     security.declarePrivate( '_getReplyResults' )
00396     def _getReplyResults( self ):
00397         """
00398            Get a list of ids of DiscussionItems which are replies to
00399            our Discussable.
00400         """
00401         discussable = self._getDiscussable()
00402         outer = self._getDiscussable( outer=1 )
00403 
00404         if discussable == outer:
00405             in_reply_to = None
00406         else:
00407             in_reply_to = discussable.getId()
00408 
00409         result = []
00410         a = result.append
00411         for key, value in self._container.items():
00412             if value.in_reply_to == in_reply_to:
00413                 a( ( key, value ) )
00414 
00415         result.sort( lambda a, b: cmp(a[1].creation_date, b[1].creation_date) )
00416 
00417         return [ x[0] for x in result ]
00418 
00419 InitializeClass(DiscussionItemContainer)