diff --git a/link/README.txt b/link/README.txt new file mode 100644 index 0000000..d6f7c13 --- /dev/null +++ b/link/README.txt @@ -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] diff --git a/link/__init__.py b/link/__init__.py new file mode 100644 index 0000000..38314f3 --- /dev/null +++ b/link/__init__.py @@ -0,0 +1,3 @@ +""" +$Id$ +""" diff --git a/link/base.py b/link/base.py new file mode 100644 index 0000000..37c898e --- /dev/null +++ b/link/base.py @@ -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 diff --git a/link/interfaces.py b/link/interfaces.py new file mode 100644 index 0000000..377de0d --- /dev/null +++ b/link/interfaces.py @@ -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. + """ diff --git a/link/tests.py b/link/tests.py new file mode 100755 index 0000000..346a36d --- /dev/null +++ b/link/tests.py @@ -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')