Back to index

plone3  3.1.7
ReferenceEngine.py
Go to the documentation of this file.
00001 import os
00002 from types import StringType, UnicodeType
00003 import time
00004 import urllib
00005 from zope.interface import implements
00006 
00007 from Products.CMFCore.utils import getToolByName
00008 from Products.Archetypes.interfaces.referenceable import IReferenceable
00009 from Products.Archetypes.interfaces import IReference
00010 from Products.Archetypes.interfaces import IReferenceCatalog
00011 from Products.Archetypes.interfaces.referenceengine import \
00012     IContentReference, IReference as Z2IReference
00013 
00014 from Products.Archetypes.utils import make_uuid, getRelURL, shasattr
00015 from Products.Archetypes.config import (
00016     TOOL_NAME, UID_CATALOG, REFERENCE_CATALOG, UUID_ATTR)
00017 from Products.Archetypes.exceptions import ReferenceException
00018 
00019 from Acquisition import aq_base, aq_parent
00020 from AccessControl import ClassSecurityInfo
00021 from OFS.SimpleItem import SimpleItem
00022 from OFS.ObjectManager import ObjectManager
00023 
00024 from Globals import InitializeClass, DTMLFile, PersistentMapping
00025 from Products.CMFCore.utils import UniqueObject
00026 from Products.CMFCore import permissions
00027 from Products.PageTemplates.PageTemplateFile import PageTemplateFile
00028 from Products.ZCatalog.ZCatalog import ZCatalog
00029 from Products.ZCatalog.Catalog import Catalog
00030 from Products import CMFCore
00031 
00032 
00033 _www = os.path.join(os.path.dirname(__file__), 'www')
00034 _catalog_dtml = os.path.join(os.path.dirname(CMFCore.__file__), 'dtml')
00035 
00036 STRING_TYPES = (StringType, UnicodeType)
00037 
00038 from Referenceable import Referenceable
00039 from UIDCatalog import UIDCatalog # Required for migrations from Plone 2.1
00040 from UIDCatalog import UIDCatalogBrains
00041 from UIDCatalog import UIDResolver
00042 
00043 class Reference(Referenceable, SimpleItem):
00044     ## Added base level support for referencing References
00045     ## They respond to the UUID protocols, but are not
00046     ## catalog aware. This means that you can't move/rename
00047     ## reference objects and expect them to work, but you can't
00048     ## do this anyway. However they should fine the correct
00049     ## events when they are added/deleted, etc
00050 
00051     __implements__ = Referenceable.__implements__ + (Z2IReference,)
00052     implements(IReference)
00053 
00054     security = ClassSecurityInfo()
00055     portal_type = 'Reference'
00056     meta_type = 'Reference'
00057 
00058     # XXX FIXME more security
00059 
00060     manage_options = (
00061         (
00062         {'label':'View', 'action':'manage_view',
00063          },
00064         )+
00065         SimpleItem.manage_options
00066         )
00067 
00068     security.declareProtected(permissions.ManagePortal,
00069                               'manage_view')
00070     manage_view = PageTemplateFile('view_reference', _www)
00071 
00072     def __init__(self, id, sid, tid, relationship, **kwargs):
00073         self.id = id
00074         setattr(self, UUID_ATTR,  id)
00075 
00076         self.sourceUID = sid
00077         self.targetUID = tid
00078         self.relationship = relationship
00079 
00080         self.__dict__.update(kwargs)
00081 
00082     def __repr__(self):
00083         return "<Reference sid:%s tid:%s rel:%s>" %(self.sourceUID, self.targetUID, self.relationship)
00084 
00085     def UID(self):
00086         """the uid method for compat"""
00087         return getattr(aq_base(self), UUID_ATTR)
00088 
00089     ###
00090     # Convenience methods
00091     def getSourceObject(self):
00092         tool = getToolByName(self, UID_CATALOG, None)
00093         if tool is None: return ''
00094         brains = tool(UID=self.sourceUID)
00095         for brain in brains:
00096             obj = brain.getObject()
00097             if obj is not None:
00098                 return obj
00099 
00100     def getTargetObject(self):
00101         tool = getToolByName(self, UID_CATALOG, None)
00102         if tool is None: return ''
00103         brains = tool(UID=self.targetUID)
00104         for brain in brains:
00105             obj = brain.getObject()
00106             if obj is not None:
00107                 return obj
00108 
00109     ###
00110     # Catalog support
00111     def targetId(self):
00112         target = self.getTargetObject()
00113         if target is not None:
00114             return target.getId()
00115         return ''
00116 
00117     def targetTitle(self):
00118         target = self.getTargetObject()
00119         if target is not None:
00120             return target.Title()
00121         return ''
00122 
00123     def Type(self):
00124         return self.__class__.__name__
00125 
00126     ###
00127     # Policy hooks, subclass away
00128     def addHook(self, tool, sourceObject=None, targetObject=None):
00129         #to reject the reference being added raise a ReferenceException
00130         pass
00131 
00132     def delHook(self, tool, sourceObject=None, targetObject=None):
00133         #to reject the delete raise a ReferenceException
00134         pass
00135 
00136     ###
00137     # OFS Operations Policy Hooks
00138     # These Hooks are experimental and subject to change
00139     def beforeTargetDeleteInformSource(self):
00140         """called before target object is deleted so
00141         the source can have a say"""
00142         pass
00143 
00144     def beforeSourceDeleteInformTarget(self):
00145         """called when the refering source Object is
00146         about to be deleted"""
00147         pass
00148 
00149     def manage_afterAdd(self, item, container):
00150         Referenceable.manage_afterAdd(self, item, container)
00151 
00152         # when copying a full site containe is the container of the plone site
00153         # and item is the plone site (at least for objects in portal root)
00154         base = container
00155         rc = getToolByName(container, REFERENCE_CATALOG)
00156         url = getRelURL(base, self.getPhysicalPath())
00157         rc.catalog_object(self, url)
00158 
00159     def manage_beforeDelete(self, item, container):
00160         Referenceable.manage_beforeDelete(self, item, container)
00161         rc  = getToolByName(container, REFERENCE_CATALOG)
00162         url = getRelURL(container, self.getPhysicalPath())
00163         rc.uncatalog_object(url)
00164 
00165 InitializeClass(Reference)
00166 
00167 REFERENCE_CONTENT_INSTANCE_NAME = 'content'
00168 
00169 class ContentReference(ObjectManager, Reference):
00170     '''Subclass of Reference to support contentish objects inside references '''
00171 
00172     __implements__ = Reference.__implements__ + (IContentReference,)
00173 
00174     def __init__(self, *args, **kw):
00175         Reference.__init__(self, *args, **kw)
00176 
00177 
00178     security = ClassSecurityInfo()
00179     # XXX FIXME more security
00180 
00181     def addHook(self, *args, **kw):
00182         # creates the content instance
00183         if type(self.contentType) in (type(''),type(u'')):
00184             # type given as string
00185             tt=getToolByName(self,'portal_types')
00186             tt.constructContent(self.contentType, self,
00187                                 REFERENCE_CONTENT_INSTANCE_NAME)
00188         else:
00189             # type given as class
00190             setattr(self, REFERENCE_CONTENT_INSTANCE_NAME,
00191                     self.contentType(REFERENCE_CONTENT_INSTANCE_NAME))
00192             getattr(self, REFERENCE_CONTENT_INSTANCE_NAME)._md=PersistentMapping()
00193 
00194     def delHook(self, *args, **kw):
00195         # remove the content instance
00196         if type(self.contentType) in (type(''),type(u'')):
00197             # type given as string
00198             self._delObject(REFERENCE_CONTENT_INSTANCE_NAME)
00199         else:
00200             # type given as class
00201             delattr(self, REFERENCE_CONTENT_INSTANCE_NAME)
00202 
00203     def getContentObject(self):
00204         return getattr(self.aq_inner.aq_explicit, REFERENCE_CONTENT_INSTANCE_NAME)
00205 
00206     def manage_afterAdd(self, item, container):
00207         Reference.manage_afterAdd(self, item, container)
00208         ObjectManager.manage_afterAdd(self, item, container)
00209 
00210     def manage_beforeDelete(self, item, container):
00211         ObjectManager.manage_beforeDelete(self, item, container)
00212         Reference.manage_beforeDelete(self, item, container)
00213 
00214 InitializeClass(ContentReference)
00215 
00216 class ContentReferenceCreator:
00217     '''Helper class to construct ContentReference instances based
00218        on a certain content type '''
00219 
00220     security = ClassSecurityInfo()
00221 
00222     def __init__(self,contentType):
00223         self.contentType=contentType
00224 
00225     def __call__(self,*args,**kw):
00226         #simulates the constructor call to the reference class in addReference
00227         res=ContentReference(*args,**kw)
00228         res.contentType=self.contentType
00229 
00230         return res
00231 
00232 InitializeClass(ContentReferenceCreator)
00233 
00234 # The brains we want to use
00235 
00236 class ReferenceCatalogBrains(UIDCatalogBrains):
00237     pass
00238 
00239 
00240 class PluggableCatalog(Catalog):
00241     # Catalog overrides
00242     # smarter brains, squirrely traversal
00243 
00244     security = ClassSecurityInfo()
00245     # XXX FIXME more security
00246 
00247     def useBrains(self, brains):
00248         """Tricky brains overrides, we need to use our own class here
00249         with annotation support
00250         """
00251         class plugbrains(self.BASE_CLASS, brains):
00252             pass
00253 
00254         schema = self.schema
00255         scopy = schema.copy()
00256 
00257         scopy['data_record_id_']=len(schema.keys())
00258         scopy['data_record_score_']=len(schema.keys())+1
00259         scopy['data_record_normalized_score_']=len(schema.keys())+2
00260 
00261         plugbrains.__record_schema__ = scopy
00262 
00263         self._v_brains = brains
00264         self._v_result_class = plugbrains
00265 
00266 InitializeClass(PluggableCatalog)
00267 
00268 class ReferenceBaseCatalog(PluggableCatalog):
00269     BASE_CLASS = ReferenceCatalogBrains
00270 
00271 
00272 class IndexableObjectWrapper(object):
00273     """Wwrapper for object indexing
00274     """    
00275     def __init__(self, obj):
00276         self._obj = obj
00277                 
00278     def __getattr__(self, name):
00279         return getattr(self._obj, name)
00280         
00281     def Title(self):
00282         # TODO: dumb try to make sure UID catalog doesn't fail if Title can't be
00283         # converted to an ascii string
00284         # Title is used for sorting only, maybe we could replace it by a better
00285         # version
00286         title = self._obj.Title()
00287         try:
00288             return str(title)
00289         except UnicodeDecodeError:
00290             return self._obj.getId()
00291 
00292 
00293 class ReferenceCatalog(UniqueObject, UIDResolver, ZCatalog):
00294     """Reference catalog
00295     """
00296 
00297     id = REFERENCE_CATALOG
00298     security = ClassSecurityInfo()
00299     implements(IReferenceCatalog)
00300 
00301     manage_catalogFind = DTMLFile('catalogFind', _catalog_dtml)
00302     manage_options = ZCatalog.manage_options
00303 
00304     # XXX FIXME more security
00305 
00306     manage_options = ZCatalog.manage_options + \
00307         ({'label': 'Rebuild catalog',
00308          'action': 'manage_rebuildCatalog',}, )
00309 
00310     def __init__(self, id, title='', vocab_id=None, container=None):
00311         """We hook up the brains now"""
00312         ZCatalog.__init__(self, id, title, vocab_id, container)
00313         self._catalog = ReferenceBaseCatalog()
00314 
00315     ###
00316     ## Public API
00317     def addReference(self, source, target, relationship=None,
00318                      referenceClass=None, updateReferences=True, **kwargs):
00319         sID, sobj = self._uidFor(source)
00320         if not sID or sobj is None:
00321             raise ReferenceException('Invalid source UID')
00322 
00323         tID, tobj = self._uidFor(target)
00324         if not tID or tobj is None:
00325             raise ReferenceException('Invalid target UID')
00326 
00327         if updateReferences:
00328             objects = self._resolveBrains(
00329                 self._queryFor(sID, tID, relationship))
00330             if objects:
00331                 #we want to update the existing reference
00332                 existing = objects[0]
00333                 if existing:
00334                     # We can't del off self, we now need to remove it
00335                     # from the source objects annotation, which we have
00336                     annotation = sobj._getReferenceAnnotations()
00337                     annotation._delObject(existing.id)
00338 
00339 
00340         rID = self._makeName(sID, tID)
00341         if not referenceClass:
00342             referenceClass = Reference
00343 
00344         annotation = sobj._getReferenceAnnotations()
00345 
00346         referenceObject = referenceClass(rID, sID, tID, relationship,
00347                                          **kwargs)
00348         # Must be wrapped into annotation context, or else
00349         # it will get indexed *twice*, one time with the wrong path.
00350         referenceObject = referenceObject.__of__(annotation)
00351         try:
00352             referenceObject.addHook(self, sobj, tobj)
00353         except ReferenceException:
00354             pass
00355         else:
00356             # This should call manage_afterAdd
00357             annotation._setObject(rID, referenceObject)
00358             return referenceObject
00359 
00360     def deleteReference(self, source, target, relationship=None):
00361         sID, sobj = self._uidFor(source)
00362         tID, tobj = self._uidFor(target)
00363 
00364         objects = self._resolveBrains(self._queryFor(sID, tID, relationship))
00365         if objects:
00366             self._deleteReference(objects[0])
00367 
00368     def deleteReferences(self, object, relationship=None):
00369         """delete all the references held by an object"""
00370         for b in self.getReferences(object, relationship):
00371             self._deleteReference(b)
00372 
00373         for b in self.getBackReferences(object, relationship):
00374             self._deleteReference(b)
00375 
00376     def getReferences(self, object, relationship=None, targetObject=None):
00377         """return a collection of reference objects"""
00378         sID, sobj = self._uidFor(object)
00379         if targetObject:
00380             tID, tobj = self._uidFor(targetObject)
00381         else:
00382             tID, tobj = None,None
00383             
00384         brains = self._queryFor(sid=sID, relationship=relationship, tid=tID)
00385         return self._resolveBrains(brains)
00386 
00387     def getBackReferences(self, object, relationship=None, targetObject=None):
00388         """return a collection of reference objects"""
00389         # Back refs would be anything that target this object
00390         sID, sobj = self._uidFor(object)
00391         if targetObject:
00392             tID, tobj = self._uidFor(targetObject)
00393         else:
00394             tID, tobj = None,None
00395 
00396         brains = self._queryFor(tid=sID, relationship=relationship, sid=tID)
00397         return self._resolveBrains(brains)
00398 
00399     def hasRelationshipTo(self, source, target, relationship):
00400         sID, sobj = self._uidFor(source)
00401         tID, tobj = self._uidFor(target)
00402 
00403         brains = self._queryFor(sID, tID, relationship)
00404         for brain in brains:
00405             obj = brain.getObject()
00406             if obj is not None:
00407                 return True
00408         return False
00409 
00410     def getRelationships(self, object):
00411         """
00412         Get all relationship types this object has TO other objects
00413         """
00414         sID, sobj = self._uidFor(object)
00415         brains = self._queryFor(sid=sID)
00416         res = {}
00417         for brain in brains:
00418             res[brain.relationship] = 1
00419 
00420         return res.keys()
00421 
00422     def getBackRelationships(self, object):
00423         """
00424         Get all relationship types this object has FROM other objects
00425         """
00426         sID, sobj = self._uidFor(object)
00427         brains = self._queryFor(tid=sID)
00428         res = {}
00429         for b in brains:
00430             res[b.relationship]=1
00431 
00432         return res.keys()
00433 
00434 
00435     def isReferenceable(self, object):
00436         return (IReferenceable.isImplementedBy(object) or
00437                 shasattr(object, 'isReferenceable'))
00438 
00439     def reference_url(self, object):
00440         """return a url to an object that will resolve by reference"""
00441         sID, sobj = self._uidFor(object)
00442         return "%s/lookupObject?uuid=%s" % (self.absolute_url(), sID)
00443 
00444     def lookupObject(self, uuid, REQUEST=None):
00445         """Lookup an object by its uuid"""
00446         obj = self._objectByUUID(uuid)
00447         if REQUEST:
00448             return REQUEST.RESPONSE.redirect(obj.absolute_url())
00449         else:
00450             return obj
00451 
00452     #####
00453     ## UID register/unregister
00454     security.declareProtected(permissions.ModifyPortalContent, 'registerObject')
00455     def registerObject(self, object):
00456         self._uidFor(object)
00457 
00458     security.declareProtected(permissions.ModifyPortalContent, 'unregisterObject')
00459     def unregisterObject(self, object):
00460         self.deleteReferences(object)
00461         uc = getToolByName(self, UID_CATALOG)
00462         uc.uncatalog_object(object._getURL())
00463 
00464 
00465     ######
00466     ## Private/Internal
00467     def _objectByUUID(self, uuid):
00468         tool = getToolByName(self, UID_CATALOG)
00469         brains = tool(UID=uuid)
00470         for brain in brains:
00471             obj = brain.getObject()
00472             if obj is not None:
00473                 return obj
00474         else:
00475             return None
00476 
00477     def _queryFor(self, sid=None, tid=None, relationship=None,
00478                   targetId=None, merge=1):
00479         """query reference catalog for object matching the info we are
00480         given, returns brains
00481 
00482         Note: targetId is the actual id of the target object, not its UID
00483         """
00484 
00485         query = {}
00486         if sid: query['sourceUID'] = sid
00487         if tid: query['targetUID'] = tid
00488         if relationship: query['relationship'] = relationship
00489         if targetId: query['targetId'] = targetId
00490         brains = self.searchResults(query, merge=merge)
00491 
00492         return brains
00493 
00494 
00495     def _uidFor(self, obj):
00496         # We should really check for the interface but I have an idea
00497         # about simple annotated objects I want to play out
00498         if type(obj) not in STRING_TYPES:
00499             uobject = aq_base(obj)
00500             if not self.isReferenceable(uobject):
00501                 raise ReferenceException, "%r not referenceable" % uobject
00502 
00503             # shasattr() doesn't work here
00504             if not getattr(aq_base(uobject), UUID_ATTR, None):
00505                 uuid = self._getUUIDFor(uobject)
00506             else:
00507                 uuid = getattr(uobject, UUID_ATTR)
00508         else:
00509             uuid = obj
00510             obj = None
00511             #and we look up the object
00512             uid_catalog = getToolByName(self, UID_CATALOG)
00513             brains = uid_catalog(UID=uuid)
00514             for brain in brains:
00515                 res = brain.getObject()
00516                 if res is not None:
00517                     obj = res
00518         return uuid, obj
00519 
00520     def _getUUIDFor(self, object):
00521         """generate and attach a new uid to the object returning it"""
00522         uuid = make_uuid(object.getId())
00523         setattr(object, UUID_ATTR, uuid)
00524 
00525         return uuid
00526 
00527     def _deleteReference(self, referenceObject):
00528         try:
00529             sobj = referenceObject.getSourceObject()
00530             referenceObject.delHook(self, sobj,
00531                                     referenceObject.getTargetObject())
00532         except ReferenceException:
00533             pass
00534         else:
00535             annotation = sobj._getReferenceAnnotations()
00536             try:
00537                 annotation._delObject(referenceObject.UID())
00538             except (AttributeError, KeyError):
00539                 pass
00540 
00541     def _resolveBrains(self, brains):
00542         objects = []
00543         if brains:
00544             objects = [b.getObject() for b in brains]
00545             objects = [b for b in objects if b]
00546         return objects
00547 
00548     def _makeName(self, *args):
00549         """get a uuid"""
00550         name = make_uuid(*args)
00551         return name
00552 
00553     def __nonzero__(self):
00554         return 1
00555 
00556     def _catalogReferencesFor(self,obj,path):
00557         if IReferenceable.isImplementedBy(obj):
00558             obj._catalogRefs(self)
00559 
00560     def _catalogReferences(self,root=None,**kw):
00561         ''' catalogs all references, where the optional parameter 'root'
00562            can be used to specify the tree that has to be searched for references '''
00563 
00564         if not root:
00565             root=getToolByName(self,'portal_url').getPortalObject()
00566 
00567         path = '/'.join(root.getPhysicalPath())
00568 
00569         results = self.ZopeFindAndApply(root,
00570                                         search_sub=1,
00571                                         apply_func=self._catalogReferencesFor,
00572                                         apply_path=path,**kw)
00573 
00574 
00575 
00576     def manage_catalogFoundItems(self, REQUEST, RESPONSE, URL2, URL1,
00577                                  obj_metatypes=None,
00578                                  obj_ids=None, obj_searchterm=None,
00579                                  obj_expr=None, obj_mtime=None,
00580                                  obj_mspec=None, obj_roles=None,
00581                                  obj_permission=None):
00582 
00583         """ Find object according to search criteria and Catalog them
00584         """
00585 
00586 
00587         elapse = time.time()
00588         c_elapse = time.clock()
00589 
00590         words = 0
00591         obj = REQUEST.PARENTS[1]
00592 
00593         self._catalogReferences(obj,obj_metatypes=obj_metatypes,
00594                                  obj_ids=obj_ids, obj_searchterm=obj_searchterm,
00595                                  obj_expr=obj_expr, obj_mtime=obj_mtime,
00596                                  obj_mspec=obj_mspec, obj_roles=obj_roles,
00597                                  obj_permission=obj_permission)
00598 
00599         elapse = time.time() - elapse
00600         c_elapse = time.clock() - c_elapse
00601 
00602         RESPONSE.redirect(
00603             URL1 +
00604             '/manage_catalogView?manage_tabs_message=' +
00605             urllib.quote('Catalog Updated\n'
00606                          'Total time: %s\n'
00607                          'Total CPU time: %s'
00608                          % (`elapse`, `c_elapse`))
00609             )
00610 
00611     security.declareProtected(permissions.ManagePortal, 'manage_rebuildCatalog')
00612     def manage_rebuildCatalog(self, REQUEST=None, RESPONSE=None):
00613         """
00614         """
00615         elapse = time.time()
00616         c_elapse = time.clock()
00617 
00618         atool = getToolByName(self, TOOL_NAME)
00619         obj = aq_parent(self)
00620         if not REQUEST:
00621             REQUEST = self.REQUEST
00622 
00623         # build a list of archetype meta types
00624         mt = tuple([typ['meta_type'] for typ in atool.listRegisteredTypes()])
00625 
00626         # clear the catalog
00627         self.manage_catalogClear()
00628 
00629         # find and catalog objects
00630         self._catalogReferences(obj,
00631                                 obj_metatypes=mt,
00632                                 REQUEST=REQUEST)
00633 
00634         elapse = time.time() - elapse
00635         c_elapse = time.clock() - c_elapse
00636 
00637         if RESPONSE:
00638             RESPONSE.redirect(
00639             REQUEST.URL1 +
00640             '/manage_catalogView?manage_tabs_message=' +
00641             urllib.quote('Catalog Rebuilded\n'
00642                          'Total time: %s\n'
00643                          'Total CPU time: %s'
00644                          % (`elapse`, `c_elapse`))
00645             )
00646 
00647 InitializeClass(ReferenceCatalog)
00648 
00649 
00650 def manage_addReferenceCatalog(self, id, title,
00651                                vocab_id=None, # Deprecated
00652                                REQUEST=None):
00653     """Add a ReferenceCatalog object
00654     """
00655     id=str(id)
00656     title=str(title)
00657     c=ReferenceCatalog(id, title, vocab_id, self)
00658     self._setObject(id, c)
00659     if REQUEST is not None:
00660         return self.manage_main(self, REQUEST,update_menu=1)
00661