Back to index

plone3  3.1.7
PloneTool.py
Go to the documentation of this file.
00001 import re
00002 import sys
00003 from types import UnicodeType, StringType
00004 import urlparse
00005 import transaction
00006 
00007 from zope.deprecation import deprecate
00008 from zope.interface import implements
00009 
00010 from AccessControl import ClassSecurityInfo, Unauthorized
00011 from Acquisition import aq_base, aq_inner, aq_parent
00012 from ComputedAttribute import ComputedAttribute
00013 from DateTime import DateTime
00014 from Globals import InitializeClass
00015 from OFS.SimpleItem import SimpleItem
00016 from OFS.ObjectManager import bad_id
00017 from ZODB.POSException import ConflictError
00018 
00019 from Products.CMFCore.utils import UniqueObject
00020 from Products.CMFCore.utils import getToolByName
00021 from Products.CMFCore import permissions
00022 from Products.CMFCore.permissions import AccessContentsInformation, \
00023                         ManagePortal, ManageUsers, ModifyPortalContent, View
00024 from Products.CMFCore.interfaces.DublinCore import DublinCore, MutableDublinCore
00025 from Products.CMFCore.interfaces.Discussions import Discussable
00026 from Products.CMFCore.WorkflowCore import WorkflowException
00027 from Products.CMFDefault.DublinCore import DefaultDublinCoreImpl
00028 from Products.CMFPlone.interfaces import IPloneTool
00029 from Products.CMFPlone.interfaces.Translatable import ITranslatable
00030 from Products.CMFPlone.interfaces import INonStructuralFolder
00031 from Products.CMFPlone.PloneBaseTool import PloneBaseTool
00032 from Products.CMFPlone.PloneFolder import ReplaceableWrapper
00033 from Products.CMFPlone import ToolNames
00034 from Products.CMFPlone import utils
00035 from Products.CMFPlone.utils import log
00036 from Products.CMFPlone.utils import log_exc
00037 from Products.CMFPlone.utils import transaction_note
00038 from Products.CMFPlone.utils import base_hasattr
00039 from Products.CMFPlone.utils import safe_hasattr
00040 from Products.CMFPlone.interfaces import IBrowserDefault
00041 from Products.statusmessages.interfaces import IStatusMessage
00042 from AccessControl.requestmethod import postonly
00043 from plone.app.linkintegrity.exceptions import LinkIntegrityNotificationException
00044 
00045 AllowSendto = 'Allow sendto'
00046 permissions.setDefaultRoles(AllowSendto, ('Anonymous', 'Manager',))
00047 
00048 _marker = utils._marker
00049 _icons = {}
00050 
00051 CEILING_DATE = DefaultDublinCoreImpl._DefaultDublinCoreImpl__CEILING_DATE
00052 FLOOR_DATE = DefaultDublinCoreImpl._DefaultDublinCoreImpl__FLOOR_DATE
00053 BAD_CHARS = re.compile(r'[^a-zA-Z0-9-_~,.$\(\)# ]').findall
00054 
00055 # XXX Remove this when we don't depend on python2.1 any longer,
00056 # use email.Utils.getaddresses instead
00057 from rfc822 import AddressList
00058 def _getaddresses(fieldvalues):
00059     """Return a list of (REALNAME, EMAIL) for each fieldvalue."""
00060     all = ', '.join(fieldvalues)
00061     a = AddressList(all)
00062     return a.addresslist
00063 
00064 # dublic core accessor name -> metadata name
00065 METADATA_DCNAME = {
00066     # The first two rows are handle in a special way
00067     # 'Description'      : 'description',
00068     # 'Subject'          : 'keywords',
00069     'Description'      : 'DC.description',
00070     'Subject'          : 'DC.subject',
00071     'Creator'          : 'DC.creator',
00072     'Contributors'     : 'DC.contributors',
00073     'Publisher'        : 'DC.publisher',
00074     'CreationDate'     : 'DC.date.created',
00075     'ModificationDate' : 'DC.date.modified',
00076     'Type'             : 'DC.type',
00077     'Format'           : 'DC.format',
00078     'Language'         : 'DC.language',
00079     'Rights'           : 'DC.rights',
00080     }
00081 
00082 
00083 class PloneTool(PloneBaseTool, UniqueObject, SimpleItem):
00084     """Various utility methods."""
00085 
00086     id = 'plone_utils'
00087     meta_type = ToolNames.UtilsTool
00088     toolicon = 'skins/plone_images/site_icon.gif'
00089     security = ClassSecurityInfo()
00090     plone_tool = 1
00091     # Prefix for forms fields!?
00092     field_prefix = 'field_'
00093 
00094     implements(IPloneTool)
00095 
00096     __implements__ = (PloneBaseTool.__implements__,
00097                       SimpleItem.__implements__, )
00098 
00099     security.declareProtected(ManageUsers, 'setMemberProperties')
00100     def setMemberProperties(self, member, REQUEST=None, **properties):
00101         pas = getToolByName(self, 'acl_users')
00102         if safe_hasattr(member, 'getId'):
00103             member = member.getId()
00104         user = pas.getUserById(member)
00105         user.setProperties(**properties)
00106 
00107     security.declarePublic('getSiteEncoding')
00108     def getSiteEncoding(self):
00109         """ Get the default_charset or fallback to utf8.
00110 
00111         >>> ptool = self.portal.plone_utils
00112 
00113         >>> ptool.getSiteEncoding()
00114         'utf-8'
00115         """
00116         return utils.getSiteEncoding(self)
00117 
00118     security.declarePublic('portal_utf8')
00119     def portal_utf8(self, str, errors='strict'):
00120         """ Transforms an string in portal encoding to utf8.
00121 
00122         >>> ptool = self.portal.plone_utils
00123         >>> text = u'Eksempel \xe6\xf8\xe5'
00124         >>> sitetext = text.encode(ptool.getSiteEncoding())
00125 
00126         >>> ptool.portal_utf8(sitetext) == text.encode('utf-8')
00127         True
00128         """
00129         return utils.portal_utf8(self, str, errors)
00130 
00131     security.declarePublic('utf8_portal')
00132     def utf8_portal(self, str, errors='strict'):
00133         """ Transforms an utf8 string to portal encoding.
00134 
00135         >>> ptool = self.portal.plone_utils
00136         >>> text = u'Eksempel \xe6\xf8\xe5'
00137         >>> utf8text = text.encode('utf-8')
00138 
00139         >>> ptool.utf8_portal(utf8text) == text.encode(ptool.getSiteEncoding())
00140         True
00141         """
00142         return utils.utf8_portal(self, str, errors)
00143 
00144     security.declarePrivate('getMailHost')
00145     def getMailHost(self):
00146         """ Gets the MailHost.
00147 
00148         >>> ptool = self.portal.plone_utils
00149 
00150         >>> ptool.getMailHost()
00151         <SecureMailHost ...>
00152         """
00153         return getattr(aq_parent(self), 'MailHost')
00154 
00155     security.declareProtected(AllowSendto, 'sendto')
00156     def sendto(self, send_to_address, send_from_address, comment,
00157                subject='Plone', **kwargs):
00158         """Sends a link of a page to someone."""
00159         host = self.getMailHost()
00160         template = getattr(self, 'sendto_template')
00161         portal = getToolByName(self, 'portal_url').getPortalObject()
00162         encoding = portal.getProperty('email_charset')
00163         if 'envelope_from' in kwargs:
00164             envelope_from = kwargs['envelope_from']
00165         else:
00166             envelope_from = send_from_address
00167         # Cook from template
00168         message = template(self, send_to_address=send_to_address,
00169                            send_from_address=send_from_address,
00170                            comment=comment, subject=subject, **kwargs)
00171         result = host.secureSend(message, send_to_address,
00172                                  envelope_from, subject=subject,
00173                                  subtype='plain', charset=encoding,
00174                                  debug=False, From=send_from_address)
00175 
00176     security.declarePublic('validateSingleNormalizedEmailAddress')
00177     def validateSingleNormalizedEmailAddress(self, address):
00178         """Lower-level function to validate a single normalized email address,
00179         see validateEmailAddress.
00180         """
00181         host = self.getMailHost()
00182         return host.validateSingleNormalizedEmailAddress(address)
00183 
00184     security.declarePublic('validateSingleEmailAddress')
00185     def validateSingleEmailAddress(self, address):
00186         """Validate a single email address, see also validateEmailAddresses."""
00187         host = self.getMailHost()
00188         return host.validateSingleEmailAddress(address)
00189 
00190     security.declarePublic('validateEmailAddresses')
00191     def validateEmailAddresses(self, addresses):
00192         """Validate a list of possibly several email addresses, see also
00193         validateSingleEmailAddress.
00194         """
00195         host = self.getMailHost()
00196         return host.validateEmailAddresses(addresses)
00197 
00198     security.declarePublic('editMetadata')
00199     def editMetadata(self
00200                      , obj
00201                      , allowDiscussion=None
00202                      , title=None
00203                      , subject=None
00204                      , description=None
00205                      , contributors=None
00206                      , effective_date=None
00207                      , expiration_date=None
00208                      , format=None
00209                      , language=None
00210                      , rights=None
00211                      ,  **kwargs):
00212         """Responsible for setting metadata on a content object.
00213 
00214         We assume the obj implements IDublinCoreMetadata.
00215         """
00216         mt = getToolByName(self, 'portal_membership')
00217         if not mt.checkPermission(ModifyPortalContent, obj):
00218             # FIXME: Some scripts rely on this being string?
00219             raise Unauthorized
00220 
00221         REQUEST = self.REQUEST
00222         pfx = self.field_prefix
00223 
00224         def getfield(request, name, default=None, pfx=pfx):
00225             return request.form.get(pfx + name, default)
00226 
00227         def tuplify(value):
00228             return tuple(filter(None, value))
00229 
00230         if DublinCore.isImplementedBy(obj):
00231             if title is None:
00232                 title = getfield(REQUEST, 'title')
00233             if description is None:
00234                 description = getfield(REQUEST, 'description')
00235             if subject is None:
00236                 subject = getfield(REQUEST, 'subject')
00237             if subject is not None:
00238                 subject = tuplify(subject)
00239             if contributors is None:
00240                 contributors = getfield(REQUEST, 'contributors')
00241             if contributors is not None:
00242                 contributors = tuplify(contributors)
00243             if effective_date is None:
00244                 effective_date = getfield(REQUEST, 'effective_date')
00245             if effective_date == '':
00246                 effective_date = 'None'
00247             if expiration_date is None:
00248                 expiration_date = getfield(REQUEST, 'expiration_date')
00249             if expiration_date == '':
00250                 expiration_date = 'None'
00251 
00252         if Discussable.isImplementedBy(obj) or \
00253             getattr(obj, '_isDiscussable', None):
00254             disc_tool = getToolByName(self, 'portal_discussion')
00255             if allowDiscussion is None:
00256                 allowDiscussion = disc_tool.isDiscussionAllowedFor(obj)
00257                 if not safe_hasattr(obj, 'allow_discussion'):
00258                     allowDiscussion = None
00259                 allowDiscussion = REQUEST.get('allowDiscussion', allowDiscussion)
00260             if type(allowDiscussion) == StringType:
00261                 allowDiscussion = allowDiscussion.lower().strip()
00262             if allowDiscussion == 'default':
00263                 allowDiscussion = None
00264             elif allowDiscussion == 'off':
00265                 allowDiscussion = 0
00266             elif allowDiscussion == 'on':
00267                 allowDiscussion = 1
00268             disc_tool.overrideDiscussionFor(obj, allowDiscussion)
00269 
00270         if MutableDublinCore.isImplementedBy(obj):
00271             if title is not None:
00272                 obj.setTitle(title)
00273             if description is not None:
00274                 obj.setDescription(description)
00275             if subject is not None:
00276                 obj.setSubject(subject)
00277             if contributors is not None:
00278                 obj.setContributors(contributors)
00279             if effective_date is not None:
00280                 obj.setEffectiveDate(effective_date)
00281             if expiration_date is not None:
00282                 obj.setExpirationDate(expiration_date)
00283             if format is not None:
00284                 obj.setFormat(format)
00285             if language is not None:
00286                 obj.setLanguage(language)
00287             if rights is not None:
00288                 obj.setRights(rights)
00289             # Make the catalog aware of changes
00290             obj.reindexObject()
00291 
00292     def _renameObject(self, obj, id):
00293         if not id:
00294             REQUEST = self.REQUEST
00295             id = REQUEST.get('id', '')
00296             id = REQUEST.get(self.field_prefix + 'id', '')
00297         if id != obj.getId():
00298             parent = aq_parent(aq_inner(obj))
00299             parent.manage_renameObject(obj.getId(), id)
00300 
00301     def _makeTransactionNote(self, obj, msg=''):
00302         #TODO Why not aq_parent()?
00303         relative_path = '/'.join(getToolByName(self, 'portal_url').getRelativeContentPath(obj)[:-1])
00304         charset = self.getSiteEncoding()
00305         if not msg:
00306             msg = relative_path + '/' + obj.title_or_id() + ' has been modified.'
00307         if isinstance(msg, UnicodeType):
00308             # Convert unicode to a regular string for the backend write IO.
00309             # UTF-8 is the only reasonable choice, as using unicode means
00310             # that Latin-1 is probably not enough.
00311             msg = msg.encode(charset)
00312         if not transaction.get().description:
00313             transaction_note(msg)
00314 
00315     security.declarePublic('contentEdit')
00316     def contentEdit(self, obj, **kwargs):
00317         """Encapsulates how the editing of content occurs."""
00318         try:
00319             self.editMetadata(obj, **kwargs)
00320         except AttributeError, msg:
00321             log('Failure editing metadata at: %s.\n%s\n' %
00322                 (obj.absolute_url(), msg))
00323         if kwargs.get('id', None) is not None:
00324             self._renameObject(obj, id=kwargs['id'].strip())
00325         self._makeTransactionNote(obj)
00326 
00327     security.declarePublic('availableMIMETypes')
00328     def availableMIMETypes(self):
00329         """Returns a map of mimetypes.
00330 
00331         Requires mimetype registry from Archetypes >= 1.3.
00332         """
00333         mtr = getToolByName(self, 'mimetypes_registry')
00334         return mtr.list_mimetypes()
00335 
00336     security.declareProtected(View, 'getWorkflowChainFor')
00337     def getWorkflowChainFor(self, object):
00338         """Proxy the request for the chain to the workflow tool, as
00339         this method is private there.
00340         """
00341         wftool = getToolByName(self, 'portal_workflow')
00342         wfs = ()
00343         try:
00344             wfs = wftool.getChainFor(object)
00345         except ConflictError:
00346             raise
00347         except:
00348             pass
00349         return wfs
00350 
00351     security.declareProtected(View, 'getIconFor')
00352     def getIconFor(self, category, id, default=_marker):
00353         """Cache point for actionicons.getActionIcon call.
00354 
00355         Also we want to allow for a default icon id to be passed in.
00356         """
00357         # Short circuit the lookup
00358         if (category, id) in _icons.keys():
00359             return _icons[(category, id)]
00360         try:
00361             actionicons = getToolByName(self, 'portal_actionicons')
00362             iconinfo = actionicons.getActionIcon(category, id)
00363             icon = _icons.setdefault((category, id), iconinfo)
00364         except KeyError:
00365             if default is not _marker:
00366                 icon = default
00367             else:
00368                 raise
00369         # We want to return the actual object
00370         return icon
00371 
00372     security.declareProtected(View, 'getReviewStateTitleFor')
00373     def getReviewStateTitleFor(self, obj):
00374         """Utility method that gets the workflow state title for the
00375         object's review_state.
00376 
00377         Returns None if no review_state found.
00378 
00379         >>> ptool = self.portal.plone_utils
00380 
00381         >>> ptool.getReviewStateTitleFor(self.folder).lower()
00382         'public draft'
00383         """
00384         wf_tool = getToolByName(self, 'portal_workflow')
00385         wfs = ()
00386         review_states = ()
00387         objstate = None
00388         try:
00389             objstate = wf_tool.getInfoFor(obj, 'review_state')
00390             wfs = wf_tool.getWorkflowsFor(obj)
00391         except WorkflowException, e:
00392             pass
00393         if wfs:
00394             for w in wfs:
00395                 if w.states.has_key(objstate):
00396                     return w.states[objstate].title or objstate
00397         return None
00398 
00399     security.declareProtected(View, 'getDiscussionThread')
00400     def getDiscussionThread(self, discussionContainer):
00401         """Given a discussionContainer, return the thread it is in, upwards,
00402         including the parent object that is being discussed.
00403         """
00404         if safe_hasattr(discussionContainer, 'parentsInThread'):
00405             thread = discussionContainer.parentsInThread()
00406             if discussionContainer.portal_type == 'Discussion Item':
00407                 thread.append(discussionContainer)
00408         else:
00409             if discussionContainer.id=='talkback':
00410                 thread=[discussionContainer._getDiscussable()]
00411             else:
00412                 thread = [discussionContainer]
00413         return thread
00414 
00415     security.declareProtected(ManagePortal, 'setDefaultSkin')
00416     @deprecate("The setDefaultSkin method of the Plone tool has been "
00417                "deprecated and will be removed in Plone 4.0.")
00418     def setDefaultSkin(self, default_skin):
00419         """Sets the default skin."""
00420         st = getToolByName(self, 'portal_skins')
00421         st.default_skin = default_skin
00422 
00423     security.declarePublic('setCurrentSkin')
00424     @deprecate("The setCurrentSkin method of the Plone tool has been "
00425                "deprecated and will be removed in Plone 4.0.")
00426     def setCurrentSkin(self, skin_name):
00427         """Sets the current skin."""
00428         portal = getToolByName(self, 'portal_url').getPortalObject()
00429         portal.changeSkin(skin_name)
00430 
00431     security.declareProtected(ManagePortal, 'changeOwnershipOf')
00432     def changeOwnershipOf(self, object, userid, recursive=0, REQUEST=None):
00433         """Changes the ownership of an object."""
00434         membership = getToolByName(self, 'portal_membership')
00435         acl_users = getattr(self, 'acl_users')
00436         user = acl_users.getUserById(userid)
00437         if user is None:
00438             # The user could be in the top level acl_users folder in
00439             # the Zope root, in which case this should find him:
00440             user = membership.getMemberById(userid)
00441             if user is None:
00442                 raise KeyError, 'Only retrievable users in this site can be made owners.'
00443         object.changeOwnership(user, recursive)
00444 
00445         def fixOwnerRole(object, user_id):
00446             # Get rid of all other owners
00447             owners = object.users_with_local_role('Owner')
00448             for o in owners:
00449                 roles = list(object.get_local_roles_for_userid(o))
00450                 roles.remove('Owner')
00451                 if roles:
00452                     object.manage_setLocalRoles(o, roles)
00453                 else:
00454                     object.manage_delLocalRoles([o])
00455             # Fix for 1750
00456             roles = list(object.get_local_roles_for_userid(user_id))
00457             roles.append('Owner')
00458             object.manage_setLocalRoles(user_id, roles)
00459 
00460         fixOwnerRole(object, user.getId())
00461         if base_hasattr(object, 'reindexObject'):
00462             object.reindexObject()
00463 
00464         if recursive:
00465             catalog_tool = getToolByName(self, 'portal_catalog')
00466             purl = getToolByName(self, 'portal_url')
00467             _path = purl.getRelativeContentURL(object)
00468             subobjects = [b.getObject() for b in \
00469                          catalog_tool(path={'query':_path,'level':1})]
00470             for obj in subobjects:
00471                 fixOwnerRole(obj, user.getId())
00472                 if base_hasattr(obj, 'reindexObject'):
00473                     obj.reindexObject()
00474     changeOwnershipOf = postonly(changeOwnershipOf)
00475 
00476     security.declarePublic('urlparse')
00477     def urlparse(self, url):
00478         """Returns the pieces of url in a six-part tuple.
00479 
00480         See Python standard library urlparse.urlparse:
00481         http://python.org/doc/lib/module-urlparse.html
00482 
00483         >>> ptool = self.portal.plone_utils
00484 
00485         >>> ptool.urlparse('http://dev.plone.org/plone/query?milestone=2.1#foo')
00486         ('http', 'dev.plone.org', '/plone/query', '', 'milestone=2.1', 'foo')
00487         """
00488         return urlparse.urlparse(url)
00489 
00490     security.declarePublic('urlunparse')
00491     def urlunparse(self, url_tuple):
00492         """Puts a url back together again, in the manner that
00493         urlparse breaks it.
00494 
00495         See also Python standard library: urlparse.urlunparse:
00496         http://python.org/doc/lib/module-urlparse.html
00497 
00498         >>> ptool = self.portal.plone_utils
00499 
00500         >>> ptool.urlunparse(('http', 'plone.org', '/support', '', '', 'users'))
00501         'http://plone.org/support#users'
00502         """
00503         return urlparse.urlunparse(url_tuple)
00504 
00505     # Enable scripts to get the string value of an exception even if the
00506     # thrown exception is a string and not a subclass of Exception.
00507     def exceptionString(self):
00508         # Don't assign the traceback to s
00509         # (otherwise will generate a circular reference)
00510         s = sys.exc_info()[:2]
00511         if s[0] == None:
00512             return None
00513         if type(s[0]) == type(''):
00514             return s[0]
00515         return str(s[1])
00516 
00517     # Provide a way of dumping an exception to the log even if we
00518     # catch it and otherwise ignore it
00519     def logException(self):
00520         """Dumps most recent exception to the log.
00521         """
00522         log_exc()
00523 
00524     security.declarePublic('createSitemap')
00525     def createSitemap(self, context, request=None):
00526         """Returns a sitemap navtree structure.
00527         """
00528         if request is None:
00529             request = self.REQUEST
00530         return utils.createSiteMap(context, request)
00531 
00532     def _addToNavTreeResult(self, result, data):
00533         """Adds a piece of content to the result tree.
00534         """
00535         return utils.addToNavTreeResult(result, data)
00536 
00537     security.declareProtected(AccessContentsInformation, 'typesToList')
00538     def typesToList(self):
00539         return utils.typesToList(self)
00540 
00541     security.declarePublic('createNavTree')
00542     def createNavTree(self, context, sitemap=None, request=None):
00543         """Returns a structure that can be used by navigation_tree_slot.
00544         """
00545         if request is None:
00546             request = self.REQUEST
00547         return utils.createNavTree(context, request)
00548 
00549     security.declarePublic('createBreadCrumbs')
00550     def createBreadCrumbs(self, context, request=None):
00551         """Returns a structure for the portal breadcumbs.
00552         """
00553         if request is None:
00554             request = self.REQUEST
00555         return utils.createBreadCrumbs(context, request)
00556 
00557     security.declarePublic('good_id')
00558     def good_id(self, id):
00559         """Exposes ObjectManager's bad_id test to skin scripts."""
00560         m = bad_id(id)
00561         if m is not None:
00562             return 0
00563         return 1
00564 
00565     security.declarePublic('bad_chars')
00566     def bad_chars(self, id):
00567         """Returns a list of the Bad characters."""
00568         return BAD_CHARS(id)
00569 
00570     security.declarePublic('getInheritedLocalRoles')
00571     def getInheritedLocalRoles(self, here):
00572         """Returns a tuple with the acquired local roles."""
00573         portal = getToolByName(here, 'portal_url').getPortalObject()
00574         result = []
00575         cont = 1
00576         if portal != here:
00577             parent = here.aq_parent
00578             while cont:
00579                 if not getattr(parent, 'acl_users', False):
00580                     break
00581                 userroles = parent.acl_users._getLocalRolesForDisplay(parent)
00582                 for user, roles, role_type, name in userroles:
00583                     # Find user in result
00584                     found = 0
00585                     for user2, roles2, type2, name2 in result:
00586                         if user2 == user:
00587                             # Check which roles must be added to roles2
00588                             for role in roles:
00589                                 if not role in roles2:
00590                                     roles2.append(role)
00591                             found = 1
00592                             break
00593                     if found == 0:
00594                         # Add it to result and make sure roles is a list so
00595                         # we may append and not overwrite the loop variable
00596                         result.append([user, list(roles), role_type, name])
00597                 if parent == portal:
00598                     cont = 0
00599                 elif not self.isLocalRoleAcquired(parent):
00600                     # Role acquired check here
00601                     cont = 0
00602                 else:
00603                     parent = parent.aq_parent
00604 
00605         # Tuplize all inner roles
00606         for pos in range(len(result)-1,-1,-1):
00607             result[pos][1] = tuple(result[pos][1])
00608             result[pos] = tuple(result[pos])
00609 
00610         return tuple(result)
00611 
00612     #
00613     # The three methods used in determining what the default-page of a folder
00614     # is. These are:
00615     #
00616     #   - getDefaultPage(folder)
00617     #       : get id of contentish object that is default-page in the folder
00618     #   - isDefaultPage(object)
00619     #       : determine if an object is the default-page in its parent folder
00620     #   - browserDefault(object)
00621     #       : lookup rules for old-style content types
00622     #
00623 
00624     security.declarePublic('isDefaultPage')
00625     def isDefaultPage(self, obj, request=None):
00626         """Finds out if the given obj is the default page in its parent folder.
00627 
00628         Only considers explicitly contained objects, either set as index_html,
00629         with the default_page property, or using IBrowserDefault.
00630         """
00631         if request is None:
00632             request = self.REQUEST
00633         return utils.isDefaultPage(obj, request)
00634 
00635     security.declarePublic('getDefaultPage')
00636     def getDefaultPage(self, obj, request=None):
00637         """Given a folderish item, find out if it has a default-page using
00638         the following lookup rules:
00639 
00640             1. A content object called 'index_html' wins
00641             2. If the folder implements IBrowserDefault, query this
00642             3. Else, look up the property default_page on the object
00643                 - Note that in this case, the returned id may *not* be of an
00644                   object in the folder, since it could be acquired from a
00645                   parent folder or skin layer
00646             4. Else, look up the property default_page in site_properties for
00647                 magic ids and test these
00648 
00649         The id of the first matching item is then used to lookup a translation
00650         and if found, its id is returned. If no default page is set, None is
00651         returned. If a non-folderish item is passed in, return None always.
00652         """
00653         if request is None:
00654             request = self.REQUEST
00655         return utils.getDefaultPage(obj, request)
00656 
00657     security.declarePublic('addPortalMessage')
00658     def addPortalMessage(self, message, type='info', request=None):
00659         """\
00660         Call this once or more to add messages to be displayed at the
00661         top of the web page.
00662 
00663         Examples:
00664 
00665         >>> ptool = self.portal.plone_utils
00666 
00667         >>> ptool.addPortalMessage(u'A random warning message', 'warning')
00668 
00669         If no type is given it defaults to 'info'
00670         >>> ptool.addPortalMessage(u'A random info message')
00671 
00672         The arguments are:
00673             message:   a string, with the text message you want to show,
00674                        or a HTML fragment (see type='structure' below)
00675             type:      optional, defaults to 'info'. The type determines how
00676                        the message will be rendered, as it is used to select
00677                        the CSS class for the message. Predefined types are:
00678                        'info' - for informational messages
00679                        'warning' - for warning messages
00680                        'error' - for messages about restricted access or errors.
00681 
00682         Portal messages are by default rendered by the global_statusmessage.pt
00683         page template.
00684 
00685         It is also possible to add messages from page templates, as
00686         long as they are processed before the portal_message macro is
00687         called by the main template. Example:
00688 
00689           <tal:block tal:define="temp python:putils.addPortalMessage('A random info message')" />
00690         """
00691         if request is None:
00692             request = self.REQUEST
00693         IStatusMessage(request).addStatusMessage(message, type=type)
00694 
00695     security.declarePublic('showPortalMessages')
00696     def showPortalMessages(self, request=None):
00697         """\
00698         Return portal status messages that will be displayed when the
00699         response web page is rendered. Portal status messages are by default
00700         rendered by the global_statusmessage.pt page template. They will be
00701         removed after they have been shown.
00702         
00703         See addPortalMessages for examples.
00704         """
00705         if request is None:
00706             request = self.REQUEST
00707         return IStatusMessage(request).showStatusMessages()
00708 
00709     security.declarePublic('browserDefault')
00710     def browserDefault(self, obj):
00711         """Sets default so we can return whatever we want instead of index_html.
00712 
00713         This method is complex, and interacts with mechanisms such as
00714         IBrowserDefault (implemented in CMFDynamicViewFTI), LinguaPlone and
00715         various mechanisms for setting the default page.
00716 
00717         The method returns a tuple (obj, [path]) where path is a path to
00718         a template or other object to be acquired and displayed on the object.
00719         The path is determined as follows:
00720 
00721         0. If we're coming from WebDAV, make sure we don't return a contained
00722             object "default page" ever
00723         1. If there is an index_html attribute (either a contained object or
00724             an explicit attribute) on the object, return that as the
00725             "default page". Note that this may be used by things like
00726             File and Image to return the contents of the file, for example,
00727             not just content-space objects created by the user.
00728         2. If the object implements IBrowserDefault, query this for the
00729             default page.
00730         3. If the object has a property default_page set and this gives a list
00731             of, or single, object id, and that object is is found in the
00732             folder or is the name of a skin template, return that id
00733         4. If the property default_page is set in site_properties and that
00734             property contains a list of ids of which one id is found in the
00735             folder, return that id
00736         5. If the object implements IBrowserDefault, try to get the selected
00737             layout.
00738         6. If the type has a 'folderlisting' action and no default page is
00739             set, use this action. This permits folders to have the default
00740             'view' action be 'string:${object_url}/' and hence default to
00741             a default page when clicking the 'view' tab, whilst allowing the
00742             fallback action to be specified TTW in portal_types (this action
00743             is typically hidden)
00744         7. If nothing else is found, fall back on the object's 'view' action.
00745         8. If this is not found, raise an AttributeError
00746 
00747         If the returned path is an object, it is checked for ITranslatable. An
00748         object which supports translation will then be translated before return.
00749         """
00750 
00751         # WebDAV in Zope is odd it takes the incoming verb eg: PROPFIND
00752         # and then requests that object, for example for: /, with verb PROPFIND
00753         # means acquire PROPFIND from the folder and call it
00754         # its all very odd and WebDAV'y
00755         request = getattr(self, 'REQUEST', None)
00756         if request and request.has_key('REQUEST_METHOD'):
00757             if request['REQUEST_METHOD'] not in  ['GET', 'POST']:
00758                 return obj, [request['REQUEST_METHOD']]
00759         # Now back to normal
00760 
00761         portal = getToolByName(self, 'portal_url').getPortalObject()
00762         wftool = getToolByName(self, 'portal_workflow')
00763 
00764         # Looking up translatable is done several places so we make a
00765         # method for it.
00766         def returnPage(obj, page):
00767             # Only look up for untranslated folderish content,
00768             # in translated containers we assume the container has default page
00769             # in the correct language.
00770             implemented = ITranslatable.isImplementedBy(obj)
00771             if not implemented or implemented and not obj.isTranslation():
00772                 pageobj = getattr(obj, page, None)
00773                 if pageobj is not None and ITranslatable.isImplementedBy(pageobj):
00774                     translation = pageobj.getTranslation()
00775                     if translation is not None and \
00776                        (not wftool.getChainFor(pageobj) or\
00777                            wftool.getInfoFor(pageobj, 'review_state') == wftool.getInfoFor(translation, 'review_state')):
00778                         if ids.has_key(translation.getId()):
00779                             return obj, [translation.getId()]
00780                         else:
00781                             return translation, ['view']
00782             return obj, [page]
00783 
00784         # The list of ids where we look for default
00785         ids = {}
00786 
00787         # If we are not dealing with a folder, then leave this empty
00788         if obj.isPrincipiaFolderish:
00789             # For BTreeFolders we just use has_key, otherwise build a dict
00790             if base_hasattr(obj, 'has_key'):
00791                 ids = obj
00792             else:
00793                 for id in obj.objectIds():
00794                     ids[id] = 1
00795 
00796         #
00797         # 1. Get an attribute or contained object index_html
00798         #
00799 
00800         # Note: The base PloneFolder, as well as ATCT's ATCTOrderedFolder
00801         # defines a method index_html() which returns a ReplaceableWrapper.
00802         # This is needed for WebDAV to work properly, and to avoid implicit
00803         # acquisition of index_html's, which are generally on-object only.
00804         # For the purposes of determining a default page, we don't want to
00805         # use this index_html(), nor the ComputedAttribute which defines it.
00806 
00807         if not isinstance(getattr(obj, 'index_html', None), ReplaceableWrapper):
00808             index_obj = getattr(aq_base(obj), 'index_html', None)
00809             if index_obj is not None and not isinstance(index_obj, ComputedAttribute):
00810                 return returnPage(obj, 'index_html')
00811 
00812         #
00813         # 2. Look for a default_page managed by an IBrowserDefault-implementing
00814         #    object
00815         #
00816         # 3. Look for a default_page property on the object
00817         #
00818         # 4. Try the default sitewide default_page setting
00819         #
00820 
00821         if obj.isPrincipiaFolderish:
00822             defaultPage = self.getDefaultPage(obj)
00823             if defaultPage is not None:
00824                 if ids.has_key(defaultPage):
00825                     return returnPage(obj, defaultPage)
00826                 # Avoid infinite recursion in the case that the page id == the
00827                 # object id
00828                 elif defaultPage != obj.getId() and \
00829                      defaultPage != '/'.join(obj.getPhysicalPath()):
00830                     # For the default_page property, we may get things in the
00831                     # skin layers or with an explicit path - split this path
00832                     # to comply with the __browser_default__() spec
00833                     return obj, defaultPage.split('/')
00834 
00835         # 5. If there is no default page, try IBrowserDefault.getLayout()
00836 
00837         browserDefault = IBrowserDefault(obj, None)
00838         if browserDefault is not None:
00839             layout = browserDefault.getLayout()
00840             if layout is None:
00841                 raise AttributeError(
00842                     "%s has no assigned layout, perhaps it needs an FTI"%obj)
00843             else:
00844                 return obj, [layout]
00845 
00846         #
00847         # 6. If the object has a 'folderlisting' action, use this
00848         #
00849 
00850         # This allows folders to determine in a flexible manner how they are
00851         # displayed when there is no default page, whilst still using
00852         # browserDefault() to show contained objects by default on the 'view'
00853         # action (this applies to old-style folders only, IBrowserDefault is
00854         # managed explicitly above)
00855 
00856         try:
00857             # XXX: This isn't quite right since it assumes the action
00858             # starts with ${object_url}.  Should we raise an error if
00859             # it doesn't?
00860             act = obj.getTypeInfo().getActionInfo('folder/folderlisting')['url'].split('/')[-1]
00861             return obj, [act]
00862         except ValueError:
00863             pass
00864 
00865         #
00866         # 7. Fall back on the 'view' action
00867         #
00868 
00869         try:
00870             # XXX: This isn't quite right since it assumes the action
00871             # starts with ${object_url}.  Should we raise an error if
00872             # it doesn't?
00873             act = obj.getTypeInfo().getActionInfo('object/view')['url'].split('/')[-1]
00874             return obj, [act]
00875         except ValueError:
00876             pass
00877 
00878         #
00879         # 8. If we can't find this either, raise an exception
00880         #
00881 
00882         raise AttributeError, "Failed to get a default page or view_action for %s" % (obj.absolute_url,)
00883 
00884     security.declarePublic('isTranslatable')
00885     def isTranslatable(self, obj):
00886         """Checks if a given object implements the ITranslatable interface."""
00887         return ITranslatable.isImplementedBy(obj)
00888 
00889     security.declarePublic('isStructuralFolder')
00890     def isStructuralFolder(self, obj):
00891         """Checks if a given object is a "structural folder".
00892 
00893         That is, a folderish item which does not explicitly implement
00894         INonStructuralFolder to declare that it doesn't wish to be treated
00895         as a folder by the navtree, the tab generation etc.
00896 
00897         >>> ptool = self.portal.plone_utils
00898 
00899         >>> ptool.isStructuralFolder(self.folder)
00900         True
00901         """
00902         if not obj.isPrincipiaFolderish:
00903             return False
00904         elif INonStructuralFolder.providedBy(obj):
00905             return False
00906         else:
00907             return True
00908 
00909     security.declarePublic('acquireLocalRoles')
00910     def acquireLocalRoles(self, obj, status = 1, REQUEST=None):
00911         """If status is 1, allow acquisition of local roles (regular behaviour).
00912 
00913         If it's 0, prohibit it (it will allow some kind of local role
00914         blacklisting).
00915         """
00916         mt = getToolByName(self, 'portal_membership')
00917         if not mt.checkPermission(ModifyPortalContent, obj):
00918             raise Unauthorized
00919 
00920         # Set local role status...
00921         # set the variable (or unset it if it's defined)
00922         if not status:
00923             obj.__ac_local_roles_block__ = 1
00924         else:
00925             if getattr(obj, '__ac_local_roles_block__', None):
00926                 obj.__ac_local_roles_block__ = None
00927 
00928         # Reindex the whole stuff.
00929         obj.reindexObjectSecurity()
00930     acquireLocalRoles = postonly(acquireLocalRoles)
00931 
00932     security.declarePublic('isLocalRoleAcquired')
00933     def isLocalRoleAcquired(self, obj):
00934         """Returns local role acquisition blocking status.
00935 
00936         True if normal, false if blocked.
00937         """
00938         if getattr(obj, '__ac_local_roles_block__', None):
00939             return False
00940         return True
00941 
00942     security.declarePublic('getOwnerName')
00943     def getOwnerName(self, obj):
00944         """ Returns the userid of the owner of an object.
00945 
00946         >>> ptool = self.portal.plone_utils
00947         >>> from Products.PloneTestCase.PloneTestCase import default_user
00948 
00949         >>> ptool.getOwnerName(self.folder) == default_user
00950         True
00951         """
00952         mt = getToolByName(self, 'portal_membership')
00953         if not mt.checkPermission(View, obj):
00954             raise Unauthorized
00955         return obj.getOwner().getId()
00956 
00957     security.declarePublic('normalizeString')
00958     def normalizeString(self, text, relaxed=False):
00959         """Normalizes a title to an id.
00960 
00961         normalizeString() converts a whole string to a normalized form that
00962         should be safe to use as in a url, as a css id, etc.
00963         
00964         If relaxed=True, only those characters that are illegal as URLs and
00965         leading or trailing whitespace is stripped.
00966 
00967         >>> ptool = self.portal.plone_utils
00968 
00969         >>> ptool.normalizeString("Foo bar")
00970         'foo-bar'
00971 
00972         >>> ptool.normalizeString("Foo bar", relaxed=True)
00973         'Foo bar'
00974         
00975         >>> ptool.normalizeString("Some!_are allowed, others&?:are not")
00976         'some-_are-allowed-others-are-not'
00977 
00978         >>> ptool.normalizeString("Some!_are allowed, others&?:are not")
00979         'some-_are-allowed-others-are-not'
00980 
00981         all punctuation and spacing is removed and replaced with a '-':
00982 
00983         >>> ptool.normalizeString("a string with spaces")
00984         'a-string-with-spaces'
00985 
00986         >>> ptool.normalizeString("p.u,n;c(t)u!a@t#i$o%n")
00987         'p-u-n-c-t-u-a-t-i-o-n'
00988 
00989         strings are lowercased:
00990 
00991         >>> ptool.normalizeString("UppERcaSE")
00992         'uppercase'
00993 
00994         punctuation, spaces, etc. are trimmed and multiples are reduced to just
00995         one:
00996 
00997         >>> ptool.normalizeString(" a string    ")
00998         'a-string'
00999         >>> ptool.normalizeString(">here's another!")
01000         'heres-another'
01001 
01002         >>> ptool.normalizeString("one with !@#$!@#$ stuff in the middle")
01003         'one-with-stuff-in-the-middle'
01004 
01005         the exception to all this is that if there is something that looks like a
01006         filename with an extension at the end, it will preserve the last period.
01007 
01008         >>> ptool.normalizeString("this is a file.gif")
01009         'this-is-a-file.gif'
01010 
01011         >>> ptool.normalizeString("this is. also. a file.html")
01012         'this-is-also-a-file.html'
01013 
01014         normalizeString() uses normalizeUnicode() to convert stray unicode
01015         characters. it will attempt to transliterate many of the accented
01016         letters to rough ASCII equivalents for characters that we can't
01017         transliterate, we just return the hex codes of the byte(s) in the
01018         character. not pretty, but about the best we can do.
01019 
01020         >>> ptool.normalizeString(u"\u9ad8\u8054\u5408 Chinese")
01021         '9ad880545408-chinese'
01022 
01023         >>> ptool.normalizeString(u"\uc774\ubbf8\uc9f1 Korean")
01024         'c774bbf8c9f1-korean'
01025         """
01026         return utils.normalizeString(text, context=self, relaxed=relaxed)
01027 
01028     security.declarePublic('listMetaTags')
01029     def listMetaTags(self, context):
01030         """Lists meta tags helper.
01031 
01032         Creates a mapping of meta tags -> values for the listMetaTags script.
01033         """
01034         result = {}
01035         site_props = getToolByName(self, 'portal_properties').site_properties
01036         use_all = site_props.getProperty('exposeDCMetaTags', None)
01037 
01038         if not use_all:
01039             metadata_names = {'Description': METADATA_DCNAME['Description']}
01040         else:
01041             metadata_names = METADATA_DCNAME
01042 
01043         for accessor, key in metadata_names.items():
01044             method = getattr(aq_inner(context).aq_explicit, accessor, None)
01045             if not callable(method):
01046                 continue
01047 
01048             # Catch AttributeErrors raised by some AT applications
01049             try:
01050                 value = method()
01051             except AttributeError:
01052                 value = None
01053 
01054             if not value:
01055                 # No data
01056                 continue
01057             if accessor == 'Publisher' and value == 'No publisher':
01058                 # No publisher is hardcoded (TODO: still?)
01059                 continue
01060             if isinstance(value, (list, tuple)):
01061                 # convert a list to a string
01062                 value = ', '.join(value)
01063 
01064             # Special cases
01065             if accessor == 'Description':
01066                 result['description'] = value
01067             elif accessor == 'Subject':
01068                 result['keywords'] = value
01069 
01070             if use_all:
01071                 result[key] = value
01072 
01073         if use_all:
01074             created = context.CreationDate()
01075 
01076             try:
01077                 effective = context.EffectiveDate()
01078                 if effective == 'None':
01079                     effective = None
01080                 if effective:
01081                     effective = DateTime(effective)
01082             except AttributeError:
01083                 effective = None
01084 
01085             try:
01086                 expires = context.ExpirationDate()
01087                 if expires == 'None':
01088                     expires = None
01089                 if expires:
01090                     expires = DateTime(expires)
01091             except AttributeError:
01092                 expires = None
01093 
01094             # Filter out DWIMish artifacts on effective / expiration dates
01095             if effective is not None and \
01096                effective > FLOOR_DATE and \
01097                effective != created:
01098                 eff_str = effective.Date()
01099             else:
01100                 eff_str = ''
01101 
01102             if expires is not None and expires < CEILING_DATE:
01103                 exp_str = expires.Date()
01104             else:
01105                 exp_str = ''
01106 
01107             if exp_str or exp_str:
01108                 result['DC.date.valid_range'] = '%s - %s' % (eff_str, exp_str)
01109 
01110         return result
01111 
01112     security.declarePublic('getUserFriendlyTypes')
01113     def getUserFriendlyTypes(self, typesList=[]):
01114         """Get a list of types which are considered "user friendly" for search
01115         and selection purposes.
01116 
01117         This is the list of types available in the portal, minus those defines
01118         in the types_not_searched property in site_properties, if it exists.
01119 
01120         If typesList is given, this is used as the base list; else all types
01121         from portal_types are used.
01122         """
01123 
01124         ptool = getToolByName(self, 'portal_properties')
01125         siteProperties = getattr(ptool, 'site_properties')
01126         blacklistedTypes = siteProperties.getProperty('types_not_searched', [])
01127 
01128         ttool = getToolByName(self, 'portal_types')
01129         types = typesList or ttool.listContentTypes()
01130 
01131         friendlyTypes = []
01132         for t in types:
01133             if not t in blacklistedTypes and not t in friendlyTypes:
01134                 friendlyTypes.append(t)
01135 
01136         return friendlyTypes
01137 
01138     security.declarePublic('reindexOnReorder')
01139     def reindexOnReorder(self, parent):
01140         """ Catalog ordering support """
01141 
01142         # For now we will just reindex all objects in the folder. Later we may
01143         # optimize to only reindex the objs that got moved. Ordering is more
01144         # for humans than machines, therefore the fact that this won't scale
01145         # well for btrees isn't a huge issue, since btrees are more for
01146         # machines than humans.
01147         mtool = getToolByName(self, 'portal_membership')
01148         if not mtool.checkPermission(ModifyPortalContent, parent):
01149             return
01150         cat = getToolByName(self, 'portal_catalog')
01151         cataloged_objs = cat(path = {'query':'/'.join(parent.getPhysicalPath()),
01152                                      'depth': 1})
01153         for brain in cataloged_objs:
01154             # Don't crash when the catalog contains a stale entry
01155             try:
01156                 obj = brain.getObject()
01157             except KeyError: # getObject raises since Zope 2.8
01158                 obj = None
01159 
01160             if obj is not None:
01161                 cat.reindexObject(obj,['getObjPositionInParent'],
01162                                                     update_metadata=0)
01163             else:
01164                 # Perhaps we should remove the bad entry as well?
01165                 log('Object in catalog no longer exists, cannot reindex: %s.'%
01166                                     brain.getPath())
01167 
01168     security.declarePublic('isIDAutoGenerated')
01169     def isIDAutoGenerated(self, id):
01170         """Determine if an id is autogenerated"""
01171         return utils.isIDAutoGenerated(self, id)
01172 
01173     security.declarePublic('getEmptyTitle')
01174     def getEmptyTitle(self, translated=True):
01175         """ Returns string to be used for objects with no title or id.
01176 
01177         >>> ptool = self.portal.plone_utils
01178 
01179         >>> ptool.getEmptyTitle(translated=False) == u'[\xb7\xb7\xb7]'
01180         True
01181         """
01182         return utils.getEmptyTitle(self, translated)
01183 
01184     security.declarePublic('pretty_title_or_id')
01185     def pretty_title_or_id(self, obj, empty_value=_marker):
01186         """Return the best possible title or id of an item, regardless
01187         of whether obj is a catalog brain or an object, but returning an
01188         empty title marker if the id is not set (i.e. it's auto-generated).
01189         """
01190         return utils.pretty_title_or_id(self, obj, empty_value=empty_value)
01191 
01192     security.declarePublic('getMethodAliases')
01193     def getMethodAliases(self, typeInfo):
01194         """Given an FTI, return the dict of method aliases defined on that
01195         FTI. If there are no method aliases (i.e. this FTI doesn't support it),
01196         return None"""
01197         getMethodAliases = getattr(typeInfo, 'getMethodAliases', None)
01198         if getMethodAliases is not None and utils.safe_callable(getMethodAliases):
01199             return getMethodAliases()
01200         else:
01201             return None
01202 
01203     # This is public because we don't know what permissions the user
01204     # has on the objects to be deleted.  The restrictedTraverse and
01205     # manage_delObjects calls should handle permission checks for us.
01206     security.declarePublic('deleteObjectsByPaths')
01207     def deleteObjectsByPaths(self, paths, handle_errors=True, REQUEST=None):
01208         failure = {}
01209         success = []
01210         # use the portal for traversal in case we have relative paths
01211         portal = getToolByName(self, 'portal_url').getPortalObject()
01212         traverse = portal.restrictedTraverse
01213         for path in paths:
01214             # Skip and note any errors
01215             if handle_errors:
01216                 sp = transaction.savepoint(optimistic=True)
01217             try:
01218                 obj = traverse(path)
01219                 obj_parent = aq_parent(aq_inner(obj))
01220                 obj_parent.manage_delObjects([obj.getId()])
01221                 success.append('%s (%s)' % (obj.title_or_id(), path))
01222             except ConflictError:
01223                 raise
01224             except LinkIntegrityNotificationException:
01225                 raise
01226             except Exception, e:
01227                 if handle_errors:
01228                     sp.rollback()
01229                     failure[path]= e
01230                 else:
01231                     raise
01232         transaction_note('Deleted %s' % (', '.join(success)))
01233         return success, failure
01234     deleteObjectsByPaths = postonly(deleteObjectsByPaths)
01235 
01236     security.declarePublic('transitionObjectsByPaths')
01237     def transitionObjectsByPaths(self, workflow_action, paths, comment='',
01238                                  expiration_date=None, effective_date=None,
01239                                  include_children=False, handle_errors=True,
01240                                  REQUEST=None):
01241         failure = {}
01242         # use the portal for traversal in case we have relative paths
01243         portal = getToolByName(self, 'portal_url').getPortalObject()
01244         traverse = portal.restrictedTraverse
01245         for path in paths:
01246             if handle_errors:
01247                 sp = transaction.savepoint(optimistic=True)
01248             try:
01249                 o = traverse(path, None)
01250                 if o is not None:
01251                     o.content_status_modify(workflow_action,
01252                                             comment,
01253                                             effective_date=effective_date,
01254                                             expiration_date=expiration_date)
01255             except ConflictError:
01256                 raise
01257             except Exception, e:
01258                 if handle_errors:
01259                     # skip this object but continue with sub-objects.
01260                     sp.rollback()
01261                     failure[path]= e
01262                 else:
01263                     raise
01264             if getattr(o, 'isPrincipiaFolderish', None) and include_children:
01265                 subobject_paths = ["%s/%s" % (path, id) for id in o.objectIds()]
01266                 self.transitionObjectsByPaths(workflow_action, subobject_paths,
01267                                               comment, expiration_date,
01268                                               effective_date, include_children,
01269                                               handle_errors)
01270         return failure
01271     transitionObjectsByPaths = postonly(transitionObjectsByPaths)
01272 
01273     security.declarePublic('renameObjectsByPaths')
01274     def renameObjectsByPaths(self, paths, new_ids, new_titles,
01275                              handle_errors=True, REQUEST=None):
01276         failure = {}
01277         success = {}
01278         # use the portal for traversal in case we have relative paths
01279         portal = getToolByName(self, 'portal_url').getPortalObject()
01280         traverse = portal.restrictedTraverse
01281         for i, path in enumerate(paths):
01282             new_id = new_ids[i]
01283             new_title = new_titles[i]
01284             if handle_errors:
01285                 sp = transaction.savepoint(optimistic=True)
01286             try:
01287                 obj = traverse(path, None)
01288                 obid = obj.getId()
01289                 title = obj.Title()
01290                 change_title = new_title and title != new_title
01291                 changed = False
01292                 if change_title:
01293                     obj.setTitle(new_title)
01294                     changed = True
01295                 if new_id and obid != new_id:
01296                     parent = aq_parent(aq_inner(obj))
01297                     parent.manage_renameObjects((obid,), (new_id,))
01298                     changed = True
01299                 elif change_title:
01300                     # the rename will have already triggered a reindex
01301                     obj.reindexObject()
01302                 if changed:
01303                     success[path]=(new_id,new_title)
01304             except ConflictError:
01305                 raise
01306             except Exception, e:
01307                 if handle_errors:
01308                     # skip this object but continue with sub-objects.
01309                     sp.rollback()
01310                     failure[path]= e
01311                 else:
01312                     raise
01313         transaction_note('Renamed %s' % str(success.keys()))
01314         return success, failure
01315     renameObjectsByPaths = postonly(renameObjectsByPaths)
01316 
01317 InitializeClass(PloneTool)