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