add new 'link' package for general-purpose link and relation management
git-svn-id: svn://svn.cy55.de/Zope3/src/cybertools/trunk@3678 fd906abe-77d9-0310-91a1-e0d9ade77398
This commit is contained in:
		
							parent
							
								
									e82034c31e
								
							
						
					
					
						commit
						24157bb323
					
				
					 5 changed files with 371 additions and 0 deletions
				
			
		
							
								
								
									
										55
									
								
								link/README.txt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								link/README.txt
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,55 @@ | ||||||
|  | ============================================ | ||||||
|  | General-purpose Link and Relation Management | ||||||
|  | ============================================ | ||||||
|  | 
 | ||||||
|  |   ($Id$) | ||||||
|  | 
 | ||||||
|  | Basic setup | ||||||
|  | ----------- | ||||||
|  | 
 | ||||||
|  |   >>> from zope import component | ||||||
|  |   >>> from cybertools.link.tests import IntIdsStub | ||||||
|  |   >>> intids = IntIdsStub() | ||||||
|  |   >>> component.provideUtility(intids) | ||||||
|  | 
 | ||||||
|  |   >>> from cybertools.link.base import LinkManager | ||||||
|  |   >>> links = LinkManager() | ||||||
|  | 
 | ||||||
|  | Create and link objects | ||||||
|  | ----------------------- | ||||||
|  | 
 | ||||||
|  | We create a simple class to derive objects from it. | ||||||
|  | 
 | ||||||
|  |   >>> class Page(object): | ||||||
|  |   ...     pass | ||||||
|  | 
 | ||||||
|  |   >>> p1 = Page() | ||||||
|  |   >>> p2 = Page() | ||||||
|  | 
 | ||||||
|  | These objects have to be registered with the IntIds utility. | ||||||
|  | 
 | ||||||
|  |   >>> intids.register(p1) | ||||||
|  |   0 | ||||||
|  |   >>> intids.register(p2) | ||||||
|  |   1 | ||||||
|  | 
 | ||||||
|  | Now we can create a link from p1 to p2. | ||||||
|  | Usually the link gets a name that is related to the target. | ||||||
|  | 
 | ||||||
|  |   >>> l01 = links.createLink(name='p2', source=p1, target=p2) | ||||||
|  | 
 | ||||||
|  | Let's have a look at the newly created link and the default values of some | ||||||
|  | of its attributes. | ||||||
|  | 
 | ||||||
|  |   >>> (l01.identifier, l01.source, l01.target, l01.name, l01.linkType, l01.state, | ||||||
|  |   ...  l01.relevance, l01.order) | ||||||
|  |   (1, 0, 1, 'p2', u'link', u'valid', 1.0, 0) | ||||||
|  | 
 | ||||||
|  | Query for links | ||||||
|  | --------------- | ||||||
|  | 
 | ||||||
|  | We are now able to query the link manager for links, e.g. using name and | ||||||
|  | source for finding all corresponding links on a page. | ||||||
|  | 
 | ||||||
|  |   >>> [l.identifier for l in links.query(name='p2', source=p1)] | ||||||
|  |   [1] | ||||||
							
								
								
									
										3
									
								
								link/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								link/__init__.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | ||||||
|  | """ | ||||||
|  | $Id$ | ||||||
|  | """ | ||||||
							
								
								
									
										170
									
								
								link/base.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								link/base.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,170 @@ | ||||||
|  | # | ||||||
|  | #  Copyright (c) 2010 Helmut Merz helmutm@cy55.de | ||||||
|  | # | ||||||
|  | #  This program is free software; you can redistribute it and/or modify | ||||||
|  | #  it under the terms of the GNU General Public License as published by | ||||||
|  | #  the Free Software Foundation; either version 2 of the License, or | ||||||
|  | #  (at your option) any later version. | ||||||
|  | # | ||||||
|  | #  This program is distributed in the hope that it will be useful, | ||||||
|  | #  but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | #  GNU General Public License for more details. | ||||||
|  | # | ||||||
|  | #  You should have received a copy of the GNU General Public License | ||||||
|  | #  along with this program; if not, write to the Free Software | ||||||
|  | #  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA | ||||||
|  | # | ||||||
|  | 
 | ||||||
|  | """ | ||||||
|  | A simple generic, general-purpose link management framework. | ||||||
|  | 
 | ||||||
|  | $Id$ | ||||||
|  | """ | ||||||
|  | 
 | ||||||
|  | from BTrees.IFBTree import intersection, union | ||||||
|  | from BTrees.IOBTree import IOBTree | ||||||
|  | from persistent import Persistent | ||||||
|  | from zope import component | ||||||
|  | from zope.component import adapts | ||||||
|  | from zope.index.field import FieldIndex | ||||||
|  | from zope.interface import implements | ||||||
|  | from zope.app.intid.interfaces import IIntIds | ||||||
|  | 
 | ||||||
|  | from cybertools.link.interfaces import ILinkManager, ILink | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class LinkManager(Persistent): | ||||||
|  |     """ A manager (storage, registry) for link objects. | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     implements(ILinkManager) | ||||||
|  | 
 | ||||||
|  |     uid = int | ||||||
|  | 
 | ||||||
|  |     # initialization | ||||||
|  | 
 | ||||||
|  |     def __init__(self): | ||||||
|  |         self.links = IOBTree() | ||||||
|  |         self.setupIndexes() | ||||||
|  |         self.currentId = 0 | ||||||
|  | 
 | ||||||
|  |     def setupIndexes(self): | ||||||
|  |         self.indexes = dict( | ||||||
|  |                 name=FieldIndex(), | ||||||
|  |                 source=FieldIndex(), | ||||||
|  |                 target=FieldIndex()) | ||||||
|  | 
 | ||||||
|  |     # public methods | ||||||
|  | 
 | ||||||
|  |     def createLink(self, **kw): | ||||||
|  |         if not 'name' in kw or not 'source' in kw: | ||||||
|  |             raise ValueError("At least 'name' and 'source' attributes must " | ||||||
|  |                              "be given.") | ||||||
|  |         identifier = self.generateIdentifier() | ||||||
|  |         link = Link(self, identifier) | ||||||
|  |         link.update(**kw) | ||||||
|  |         self.getLinks()[identifier] = link | ||||||
|  |         self.indexLink(link) | ||||||
|  |         return link | ||||||
|  | 
 | ||||||
|  |     def removeLink(self, link): | ||||||
|  |         del self.getLinks()[link.identifier] | ||||||
|  | 
 | ||||||
|  |     def getLink(self, identifier): | ||||||
|  |         return self.getLinks()[identifier] | ||||||
|  | 
 | ||||||
|  |     def query(self, name=None, source=None, target=None, **kw): | ||||||
|  |         source = self.getUniqueId(source) | ||||||
|  |         target = self.getUniqueId(target) | ||||||
|  |         r = None | ||||||
|  |         if name is not None: | ||||||
|  |             r = self.indexes['name'].apply((name, name)) | ||||||
|  |         if source is not None: | ||||||
|  |             r = self.intersect(r, self.indexes['source'].apply((source, source))) | ||||||
|  |         if target is not None: | ||||||
|  |             r = self.intersect(r, self.indexes['target'].apply((target, target))) | ||||||
|  |         if r is None: | ||||||
|  |             raise ValueError("At least one critera of 'name', 'source', or " | ||||||
|  |                              "'target' must be given.") | ||||||
|  |         result = [self.getLink(id) for id in r] | ||||||
|  |         #for k, v in kw: | ||||||
|  |         #    result = [link for link in result if getattr(link, k) == v] | ||||||
|  |         return sorted(result, key=lambda x: (x.order, x.name)) | ||||||
|  | 
 | ||||||
|  |     def __iter__(self): | ||||||
|  |         return self.getLinks().values() | ||||||
|  | 
 | ||||||
|  |     # protected methods | ||||||
|  | 
 | ||||||
|  |     def getLinks(self): | ||||||
|  |         return self.links | ||||||
|  | 
 | ||||||
|  |     def generateIdentifier(self): | ||||||
|  |         self.currentId += 1 | ||||||
|  |         return self.currentId | ||||||
|  | 
 | ||||||
|  |     def indexLink(self, link): | ||||||
|  |         for attr, idx in self.indexes.items(): | ||||||
|  |             value = getattr(link, attr) | ||||||
|  |             if value is None: | ||||||
|  |                 idx.unindex_doc(link.identifier) | ||||||
|  |             else: | ||||||
|  |                 idx.index_doc(link.identifier, value) | ||||||
|  | 
 | ||||||
|  |     def intersect(self, r1, r2): | ||||||
|  |         return r1 is None and r2 or intersection(r1, r2) | ||||||
|  | 
 | ||||||
|  |     def getUniqueId(self, obj): | ||||||
|  |         if obj is None: | ||||||
|  |             return None | ||||||
|  |         if isinstance(obj, self.uid): | ||||||
|  |             return obj | ||||||
|  |         # TODO: take external objects (referenced by URIs) in to account | ||||||
|  |         return component.getUtility(IIntIds).getId(obj) | ||||||
|  | 
 | ||||||
|  |     def getObject(self, uid): | ||||||
|  |         return component.getUtility(IIntIds).getObject(uid) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Link(Persistent): | ||||||
|  |     """ A basic link implementation. | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     implements(ILink) | ||||||
|  | 
 | ||||||
|  |     defaults = dict(target=None, | ||||||
|  |                     linkType=u'link', | ||||||
|  |                     state=u'valid', | ||||||
|  |                     relevance=1.0, | ||||||
|  |                     order=0,) | ||||||
|  | 
 | ||||||
|  |     def __init__(self, manager, identifier): | ||||||
|  |         self.manager = self.__parent__ = manager | ||||||
|  |         self.identifier = self.__name__ = identifier | ||||||
|  | 
 | ||||||
|  |     def getManager(self): | ||||||
|  |         return self.manager | ||||||
|  | 
 | ||||||
|  |     def getSource(self): | ||||||
|  |         return self.getManager().getObject(self.source) | ||||||
|  | 
 | ||||||
|  |     def getTarget(self): | ||||||
|  |         return self.getManager().getObject(self.target) | ||||||
|  | 
 | ||||||
|  |     def update(self, **kw): | ||||||
|  |         manager = self.getManager() | ||||||
|  |         for k in ('source', 'target'): | ||||||
|  |             kw[k] = manager.getUniqueId(kw[k]) | ||||||
|  |         for k, v in kw.items(): | ||||||
|  |             setattr(self, k, v) | ||||||
|  |         for k in manager.indexes: | ||||||
|  |             if k in kw: | ||||||
|  |                 manager.indexLink(self) | ||||||
|  |                 break | ||||||
|  | 
 | ||||||
|  |     def __getattr__(self, attr): | ||||||
|  |         if attr not in ILink: | ||||||
|  |             raise AttributeError(attr) | ||||||
|  |         value = self.defaults.get(attr, u'') | ||||||
|  |         return value | ||||||
							
								
								
									
										92
									
								
								link/interfaces.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								link/interfaces.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,92 @@ | ||||||
|  | # | ||||||
|  | #  Copyright (c) 2010 Helmut Merz helmutm@cy55.de | ||||||
|  | # | ||||||
|  | #  This program is free software; you can redistribute it and/or modify | ||||||
|  | #  it under the terms of the GNU General Public License as published by | ||||||
|  | #  the Free Software Foundation; either version 2 of the License, or | ||||||
|  | #  (at your option) any later version. | ||||||
|  | # | ||||||
|  | #  This program is distributed in the hope that it will be useful, | ||||||
|  | #  but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | #  GNU General Public License for more details. | ||||||
|  | # | ||||||
|  | #  You should have received a copy of the GNU General Public License | ||||||
|  | #  along with this program; if not, write to the Free Software | ||||||
|  | #  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA | ||||||
|  | # | ||||||
|  | 
 | ||||||
|  | """ | ||||||
|  | Interfaces for Wiki functionality. | ||||||
|  | 
 | ||||||
|  | $Id$ | ||||||
|  | """ | ||||||
|  | 
 | ||||||
|  | from zope.interface import Interface, Attribute | ||||||
|  | from zope import schema | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class ILinkManager(Interface): | ||||||
|  |     """ Manages (and possibly contains) all kinds of wiki-related links. | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     def createLink(name, source, target, **kw): | ||||||
|  |         """ Create, register, and return a link record. | ||||||
|  | 
 | ||||||
|  |             Optional attributes are given as keyword arguments. | ||||||
|  |         """ | ||||||
|  | 
 | ||||||
|  |     def removeLink(link): | ||||||
|  |         """ Remove a link. | ||||||
|  |         """ | ||||||
|  | 
 | ||||||
|  |     def getLink(identifier): | ||||||
|  |         """ Return the link record identfied by the identifier given or None if | ||||||
|  |             not present. | ||||||
|  |         """ | ||||||
|  | 
 | ||||||
|  |     def query(name=None, source=None, target=None, **kw): | ||||||
|  |         """ Search for link records matching the criteria given. One of | ||||||
|  |             name, source or target must be given. | ||||||
|  |         """ | ||||||
|  | 
 | ||||||
|  |     def __iter__(): | ||||||
|  |         """ Return an iterator of all links. | ||||||
|  |         """ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class ILink(Interface): | ||||||
|  |     """ A directed link (connection, relation) between two local or foreign | ||||||
|  |         objects. | ||||||
|  | 
 | ||||||
|  |         The combination of name and source usually uniquely identfy a link. | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     identifier = Attribute('An internal identifier of the link record, ' | ||||||
|  |                 'unique within the link manager.') | ||||||
|  |     name = Attribute('The external identifier for the link, i.e. the ' | ||||||
|  |                 'string used in the source text to address the link.') | ||||||
|  |     source = Attribute('Identifier of the link\'s source object.') | ||||||
|  |     target = Attribute('Identifier of the link\'s target object or - ' | ||||||
|  |                 'for external links - the target URI.') | ||||||
|  |     linkType = Attribute('Optional: A short string specifying the type of the ' | ||||||
|  |                 'link, a sort of predicate; default: "link".') | ||||||
|  |     title = Attribute('Optional: A short text, may be used as the default text for ' | ||||||
|  |                 'the link or for the alt tag of an image.') | ||||||
|  |     description = Attribute('Optional: some text, may be used as a title attribute.') | ||||||
|  |     state = Attribute('Optional: A short string denoting the state of the link ' | ||||||
|  |                 'entry; default: "valid"') | ||||||
|  |     relevance = Attribute('Optional: A float number between 0.0 and 1.0 denoting ' | ||||||
|  |                 'the relevance of the connection between source and target; ' | ||||||
|  |                 'default: 1.0.') | ||||||
|  |     order = Attribute('Optional: An integer that may be used when providing an ' | ||||||
|  |                 'ordered listing of links; default: 0.') | ||||||
|  |     targetFragment = Attribute('Optional: an address part leading to a ' | ||||||
|  |                 'text anchor or the part of an image.') | ||||||
|  |     targetParameters = Attribute('Optional: a dictionary of URI parameters ' | ||||||
|  |                 'that will have to be appended to the link to the target object.') | ||||||
|  |     creator = Attribute('Optional: a string denoting the creator of the record.') | ||||||
|  | 
 | ||||||
|  |     def getManager(): | ||||||
|  |         """ Return the link manager this link is managed by. | ||||||
|  |         """ | ||||||
							
								
								
									
										51
									
								
								link/tests.py
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										51
									
								
								link/tests.py
									
										
									
									
									
										Executable file
									
								
							|  | @ -0,0 +1,51 @@ | ||||||
|  | # $Id$ | ||||||
|  | 
 | ||||||
|  | import unittest | ||||||
|  | from zope.testing.doctestunit import DocFileSuite | ||||||
|  | from zope.interface.verify import verifyClass | ||||||
|  | from zope.interface import implements | ||||||
|  | from zope.app.intid.interfaces import IIntIds | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class IntIdsStub(object): | ||||||
|  |     """A testing stub (mock utility) for IntIds.""" | ||||||
|  | 
 | ||||||
|  |     implements(IIntIds) | ||||||
|  | 
 | ||||||
|  |     def __init__(self): | ||||||
|  |         self.objs = [] | ||||||
|  | 
 | ||||||
|  |     def getObject(self, uid): | ||||||
|  |         return self.objs[uid] | ||||||
|  | 
 | ||||||
|  |     def register(self, ob): | ||||||
|  |         if ob not in self.objs: | ||||||
|  |             self.objs.append(ob) | ||||||
|  |         return self.objs.index(ob) | ||||||
|  | 
 | ||||||
|  |     getId = register | ||||||
|  |     queryId = getId | ||||||
|  | 
 | ||||||
|  |     def unregister(self, ob): | ||||||
|  |         id = self.getId(ob) | ||||||
|  |         self.objs[id] = None | ||||||
|  | 
 | ||||||
|  |     def __iter__(self): | ||||||
|  |         return iter(xrange(len(self.objs))) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TestLink(unittest.TestCase): | ||||||
|  |     "Basic tests for the link package." | ||||||
|  | 
 | ||||||
|  |     def testBasics(self): | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def test_suite(): | ||||||
|  |     return unittest.TestSuite(( | ||||||
|  |                 unittest.makeSuite(TestLink), | ||||||
|  |                 DocFileSuite('README.txt'), | ||||||
|  |             )) | ||||||
|  | 
 | ||||||
|  | if __name__ == '__main__': | ||||||
|  |     unittest.main(defaultTest='test_suite') | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 helmutm
						helmutm