Back to index

plone3  3.1.7
CatalogTool.py
Go to the documentation of this file.
00001 #
00002 # Plone CatalogTool
00003 #
00004 import re
00005 import time
00006 import urllib
00007 
00008 from Products.CMFCore.CatalogTool import CatalogTool as BaseTool
00009 from Products.CMFCore.CatalogTool import IndexableObjectWrapper
00010 
00011 from Products.CMFCore.permissions import AccessInactivePortalContent
00012 from Products.CMFPlone import ToolNames
00013 from AccessControl import ClassSecurityInfo
00014 from Globals import InitializeClass
00015 from Globals import DTMLFile
00016 from Acquisition import aq_inner
00017 from Acquisition import aq_parent
00018 from Acquisition import aq_base
00019 from DateTime import DateTime
00020 from BTrees.Length import Length
00021 
00022 from Products.CMFCore.utils import _getAuthenticatedUser
00023 from Products.CMFCore.utils import _checkPermission
00024 from Products.CMFCore.utils import getToolByName
00025 from Products.CMFCore.CatalogTool import _mergedLocalRoles
00026 from Products.CMFPlone.PloneBaseTool import PloneBaseTool
00027 from Products.CMFPlone.interfaces import INonStructuralFolder
00028 from Products.CMFPlone.interfaces.NonStructuralFolder import \
00029      INonStructuralFolder as z2INonStructuralFolder
00030 from Products.CMFPlone.utils import base_hasattr
00031 from Products.CMFPlone.utils import safe_callable
00032 from Products.CMFPlone.utils import safe_unicode
00033 from OFS.IOrderSupport import IOrderedContainer
00034 from ZODB.POSException import ConflictError
00035 
00036 from Products.ZCatalog.ZCatalog import ZCatalog
00037 
00038 from AccessControl.Permissions import manage_zcatalog_entries as ManageZCatalogEntries
00039 from AccessControl.Permissions import search_zcatalog as SearchZCatalog
00040 from AccessControl.PermissionRole import rolesForPermissionOn
00041 
00042 from Products.CMFCore.interfaces import ISiteRoot
00043 
00044 from zope.interface import Interface, implements, providedBy
00045 from zope.component import adapts, getMultiAdapter
00046 
00047 from plone.app.content.interfaces import IIndexableObjectWrapper
00048 
00049 _marker = object()
00050 
00051 
00052 class ExtensibleIndexableObjectRegistry(dict):
00053     """Registry for extensible object indexing.
00054     """
00055 
00056     def register(self, name, callable):
00057         """Register a callable method for an attribute.
00058 
00059         The method will be called with the object as first argument and
00060         additional keyword arguments like portal and the workflow vars.
00061         """
00062         self[name] = callable
00063 
00064     def unregister(self, name):
00065         del self[name]
00066 
00067 _eioRegistry = ExtensibleIndexableObjectRegistry()
00068 registerIndexableAttribute = _eioRegistry.register
00069 
00070 
00071 class ExtensibleIndexableObjectWrapper(IndexableObjectWrapper):
00072     """Extensible wrapper for object indexing.
00073 
00074     vars - additional vars as a dict, used for workflow vars like review_state
00075     obj - the indexable object
00076     portal - the portal root object
00077     registry - a registry
00078     **kwargs - additional keyword arguments
00079     """
00080 
00081     implements(IIndexableObjectWrapper)
00082     adapts(Interface, ISiteRoot)
00083 
00084     def __init__(self, obj, portal, registry = _eioRegistry):
00085         # Because we want to look this up as an adapter, we defer 
00086         # initialisation until the update() method is called
00087         super(ExtensibleIndexableObjectWrapper, self).__init__({}, obj)
00088         self._portal = portal
00089         self._registry = registry
00090         self._kwargs = {}
00091 
00092     def update(self, vars, **kwargs):
00093         self._IndexableObjectWrapper__vars = vars
00094         self._kwargs = kwargs
00095         if 'registry' in kwargs:
00096             self._registry = kwargs['registry']
00097 
00098     def beforeGetattrHook(self, vars, obj, kwargs):
00099         return vars, obj, kwargs
00100 
00101     def __getattr__(self, name):
00102         vars = self._IndexableObjectWrapper__vars
00103         obj = self._IndexableObjectWrapper__ob
00104         kwargs = self._kwargs
00105         registry = self._registry
00106 
00107         vars, obj, kwargs = self.beforeGetattrHook(vars, obj, kwargs)
00108 
00109         if registry.has_key(name):
00110             return registry[name](obj, portal=self._portal, vars=vars, **kwargs)
00111         return super(ExtensibleIndexableObjectWrapper, self).__getattr__(name)
00112     
00113     def allowedRolesAndUsers(self):
00114         # Disable CMFCore version of this method; use registry hook instead
00115         return self.__getattr__('allowedRolesAndUsers')
00116 
00117 
00118 def allowedRolesAndUsers(obj, portal, **kwargs):
00119     """Return a list of roles and users with View permission.
00120 
00121     Used by PortalCatalog to filter out items you're not allowed to see.
00122     """
00123     allowed = {}
00124     for r in rolesForPermissionOn('View', obj):
00125         allowed[r] = 1
00126     try:
00127         localroles = portal.acl_users._getAllLocalRoles(obj)
00128     except AttributeError:
00129         localroles = _mergedLocalRoles(obj)
00130     for user, roles in localroles.items():
00131         for role in roles:
00132             if allowed.has_key(role):
00133                 allowed['user:' + user] = 1
00134     if allowed.has_key('Owner'):
00135         del allowed['Owner']
00136     return list(allowed.keys())
00137 
00138 registerIndexableAttribute('allowedRolesAndUsers', allowedRolesAndUsers)
00139 
00140 
00141 def object_provides(object, portal, **kw):
00142     return [i.__identifier__ for i in providedBy(object).flattened()]
00143 
00144 registerIndexableAttribute('object_provides', object_provides)
00145 
00146 
00147 def zero_fill(matchobj):
00148     return matchobj.group().zfill(8)
00149 
00150 num_sort_regex = re.compile('\d+')
00151 
00152 
00153 def sortable_title(obj, portal, **kwargs):
00154     """ Helper method for to provide FieldIndex for Title.
00155 
00156     >>> from Products.CMFPlone.CatalogTool import sortable_title
00157 
00158     >>> self.folder.setTitle('Plone42 _foo')
00159     >>> sortable_title(self.folder, self.portal)
00160     'plone00000042 _foo'
00161     """
00162     title = getattr(obj, 'Title', None)
00163     if title is not None:
00164         if safe_callable(title):
00165             title = title()
00166         if isinstance(title, basestring):
00167             sortabletitle = title.lower().strip()
00168             # Replace numbers with zero filled numbers
00169             sortabletitle = num_sort_regex.sub(zero_fill, sortabletitle)
00170             # Truncate to prevent bloat
00171             sortabletitle = safe_unicode(sortabletitle)[:30].encode('utf-8')
00172             return sortabletitle
00173     return ''
00174 
00175 registerIndexableAttribute('sortable_title', sortable_title)
00176 
00177 
00178 def getObjPositionInParent(obj, **kwargs):
00179     """ Helper method for catalog based folder contents.
00180 
00181     >>> from Products.CMFPlone.CatalogTool import getObjPositionInParent
00182 
00183     >>> getObjPositionInParent(self.folder)
00184     0
00185     """
00186     parent = aq_parent(aq_inner(obj))
00187     if IOrderedContainer.isImplementedBy(parent):
00188         try:
00189             return parent.getObjectPosition(obj.getId())
00190         except ConflictError:
00191             raise
00192         except:
00193             pass
00194             # XXX log
00195     return 0
00196 
00197 registerIndexableAttribute('getObjPositionInParent', getObjPositionInParent)
00198 
00199 
00200 SIZE_CONST = {'kB': 1024, 'MB': 1024*1024, 'GB': 1024*1024*1024}
00201 SIZE_ORDER = ('GB', 'MB', 'kB')
00202 
00203 def getObjSize(obj, **kwargs):
00204     """ Helper method for catalog based folder contents.
00205 
00206     >>> from Products.CMFPlone.CatalogTool import getObjSize
00207 
00208     >>> getObjSize(self.folder)
00209     '1 kB'
00210     """
00211     smaller = SIZE_ORDER[-1]
00212 
00213     if base_hasattr(obj, 'get_size'):
00214         size = obj.get_size()
00215     else:
00216         size = 0
00217 
00218     # if the size is a float, then make it an int
00219     # happens for large files
00220     try:
00221         size = int(size)
00222     except (ValueError, TypeError):
00223         pass
00224 
00225     if not size:
00226         return '0 %s' % smaller
00227 
00228     if isinstance(size, (int, long)):
00229         if size < SIZE_CONST[smaller]:
00230             return '1 %s' % smaller
00231         for c in SIZE_ORDER:
00232             if size/SIZE_CONST[c] > 0:
00233                 break
00234         return '%.1f %s' % (float(size/float(SIZE_CONST[c])), c)
00235     return size
00236 
00237 registerIndexableAttribute('getObjSize', getObjSize)
00238 
00239 
00240 def is_folderish(obj, **kwargs):
00241     """Should this item be treated as a folder?
00242 
00243     Checks isPrincipiaFolderish, as well as the INonStructuralFolder
00244     interfaces.
00245 
00246       >>> from Products.CMFPlone.CatalogTool import is_folderish
00247       >>> from Products.CMFPlone.interfaces import INonStructuralFolder
00248       >>> from Products.CMFPlone.interfaces.NonStructuralFolder import INonStructuralFolder as z2INonStructuralFolder
00249       >>> from zope.interface import directlyProvidedBy, directlyProvides
00250 
00251     A Folder is folderish generally::
00252       >>> is_folderish(self.folder)
00253       True
00254 
00255     But if we make it an INonStructuralFolder it is not::
00256       >>> base_implements = directlyProvidedBy(self.folder)
00257       >>> directlyProvides(self.folder, INonStructuralFolder, directlyProvidedBy(self.folder))
00258       >>> is_folderish(self.folder)
00259       False
00260       
00261     Now we revert our interface change and apply the z2 no-folderish interface::
00262       >>> directlyProvides(self.folder, base_implements)
00263       >>> is_folderish(self.folder)
00264       True
00265       >>> z2base_implements = self.folder.__implements__
00266       >>> self.folder.__implements__ = z2base_implements + (z2INonStructuralFolder,)
00267       >>> is_folderish(self.folder)
00268       False
00269 
00270     We again revert the interface change and check to make sure that
00271     PrincipiaFolderish is respected::
00272       >>> self.folder.__implements__ = z2base_implements
00273       >>> is_folderish(self.folder)
00274       True
00275       >>> self.folder.isPrincipiaFolderish = False
00276       >>> is_folderish(self.folder)
00277       False
00278 
00279     """
00280     # If the object explicitly states it doesn't want to be treated as a
00281     # structural folder, don't argue with it.
00282     folderish = bool(getattr(aq_base(obj), 'isPrincipiaFolderish', False))
00283     if not folderish:
00284         return False
00285     elif INonStructuralFolder.providedBy(obj):
00286         return False
00287     elif z2INonStructuralFolder.isImplementedBy(obj):
00288         # BBB: for z2 interface compat
00289         return False
00290     else:
00291         return folderish
00292 
00293 registerIndexableAttribute('is_folderish', is_folderish)
00294 
00295 
00296 def syndication_enabled(obj, **kwargs):
00297     """Get state of syndication.
00298     """
00299     syn = getattr(aq_base(obj), 'syndication_information', _marker)
00300     if syn is not _marker:
00301         return True
00302     return False
00303 
00304 registerIndexableAttribute('syndication_enabled', syndication_enabled)
00305 
00306 
00307 def is_default_page(obj, portal, **kwargs):
00308     """Is this the default page in its folder
00309     """
00310     ptool = getToolByName(portal, 'plone_utils')
00311     return ptool.isDefaultPage(obj)
00312 
00313 registerIndexableAttribute('is_default_page', is_default_page)
00314 
00315 
00316 def getIcon(obj, **kwargs):
00317     """Make sure we index icon relative to portal"""
00318     return obj.getIcon(True)
00319 
00320 registerIndexableAttribute('getIcon', getIcon)
00321 
00322 
00323 class CatalogTool(PloneBaseTool, BaseTool):
00324 
00325     meta_type = ToolNames.CatalogTool
00326     security = ClassSecurityInfo()
00327     toolicon = 'skins/plone_images/book_icon.gif'
00328     _counter = None
00329 
00330     manage_catalogAdvanced = DTMLFile('www/catalogAdvanced', globals())
00331 
00332     __implements__ = (PloneBaseTool.__implements__, BaseTool.__implements__)
00333 
00334     def __init__(self):
00335         ZCatalog.__init__(self, self.getId())
00336 
00337     def _removeIndex(self, index):
00338         """Safe removal of an index.
00339         """
00340         try:
00341             self.manage_delIndex(index)
00342         except:
00343             pass
00344 
00345     def _listAllowedRolesAndUsers(self, user):
00346         """Makes sure the list includes the user's groups.
00347         """
00348         result = list(user.getRoles())
00349         if hasattr(aq_base(user), 'getGroups'):
00350             result = result + ['user:%s' % x for x in user.getGroups()]
00351         result.append('Anonymous')
00352         result.append('user:%s' % user.getId())
00353         return result
00354 
00355     security.declarePrivate('indexObject')
00356     def indexObject(self, object, idxs=[]):
00357         """Add object to catalog.
00358 
00359         The optional idxs argument is a list of specific indexes
00360         to populate (all of them by default).
00361         """
00362         self.reindexObject(object, idxs)
00363 
00364     security.declareProtected(ManageZCatalogEntries, 'catalog_object')
00365     def catalog_object(self, object, uid, idxs=[],
00366                        update_metadata=1, pghandler=None):
00367         self._increment_counter()
00368         # Wraps the object with workflow and accessibility
00369         # information just before cataloging.
00370         wf = getattr(self, 'portal_workflow', None)
00371         # A comment for all the frustrated developers which aren't able to pin
00372         # point the code which adds the review_state to the catalog. :)
00373         # The review_state var and some other workflow vars are added to the
00374         # indexable object wrapper throught the code in the following lines
00375         if wf is not None:
00376             vars = wf.getCatalogVariablesFor(object)
00377         else:
00378             vars = {}
00379         portal = aq_parent(aq_inner(self))
00380         
00381         w = getMultiAdapter((object, portal), IIndexableObjectWrapper)
00382         w.update(vars)
00383         
00384         ZCatalog.catalog_object(self, w, uid, idxs,
00385                                 update_metadata, pghandler=pghandler)
00386 
00387     security.declareProtected(ManageZCatalogEntries, 'catalog_object')
00388     def uncatalog_object(self, *args, **kwargs):
00389         self._increment_counter()
00390         return BaseTool.uncatalog_object(self, *args, **kwargs)
00391 
00392     def _increment_counter(self):
00393         if self._counter is None:
00394             self._counter = Length()
00395         self._counter.change(1)
00396 
00397     security.declarePrivate('getCounter')
00398     def getCounter(self):
00399         return self._counter is not None and self._counter() or 0
00400 
00401     security.declareProtected(SearchZCatalog, 'searchResults')
00402     def searchResults(self, REQUEST=None, **kw):
00403         """Calls ZCatalog.searchResults with extra arguments that
00404         limit the results to what the user is allowed to see.
00405 
00406         This version uses the 'effectiveRange' DateRangeIndex.
00407 
00408         It also accepts a keyword argument show_inactive to disable
00409         effectiveRange checking entirely even for those without portal
00410         wide AccessInactivePortalContent permission.
00411         """
00412         kw = kw.copy()
00413         show_inactive = kw.get('show_inactive', False)
00414 
00415         user = _getAuthenticatedUser(self)
00416         kw['allowedRolesAndUsers'] = self._listAllowedRolesAndUsers(user)
00417 
00418         if not show_inactive and not _checkPermission(AccessInactivePortalContent, self):
00419             kw['effectiveRange'] = DateTime()
00420 
00421         return ZCatalog.searchResults(self, REQUEST, **kw)
00422 
00423     __call__ = searchResults
00424 
00425     security.declareProtected(ManageZCatalogEntries, 'clearFindAndRebuild')
00426     def clearFindAndRebuild(self):
00427         """Empties catalog, then finds all contentish objects (i.e. objects
00428            with an indexObject method), and reindexes them.
00429            This may take a long time.
00430         """
00431         def indexObject(obj, path):
00432             if (base_hasattr(obj, 'indexObject') and
00433                 safe_callable(obj.indexObject)):
00434                 try:
00435                     obj.indexObject()
00436                 except TypeError:
00437                     # Catalogs have 'indexObject' as well, but they
00438                     # take different args, and will fail
00439                     pass
00440         self.manage_catalogClear()
00441         portal = aq_parent(aq_inner(self))
00442         portal.ZopeFindAndApply(portal, search_sub=True, apply_func=indexObject)
00443 
00444     security.declareProtected(ManageZCatalogEntries, 'manage_catalogRebuild')
00445     def manage_catalogRebuild(self, RESPONSE=None, URL1=None):
00446         """Clears the catalog and indexes all objects with an 'indexObject' method.
00447            This may take a long time.
00448         """
00449         elapse = time.time()
00450         c_elapse = time.clock()
00451 
00452         self.clearFindAndRebuild()
00453 
00454         elapse = time.time() - elapse
00455         c_elapse = time.clock() - c_elapse
00456 
00457         if RESPONSE is not None:
00458             RESPONSE.redirect(
00459               URL1 + '/manage_catalogAdvanced?manage_tabs_message=' +
00460               urllib.quote('Catalog Rebuilt\n'
00461                            'Total time: %s\n'
00462                            'Total CPU time: %s' % (`elapse`, `c_elapse`)))
00463 
00464 CatalogTool.__doc__ = BaseTool.__doc__
00465 
00466 InitializeClass(CatalogTool)