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