diff --git a/browser/flash/configure.zcml b/browser/flash/configure.zcml
index 1809a80..3646e84 100644
--- a/browser/flash/configure.zcml
+++ b/browser/flash/configure.zcml
@@ -5,6 +5,8 @@
xmlns="http://namespaces.zope.org/browser"
i18n_domain="zope">
+
+
+ logoURL context/++resource++loops_logo.jpg;
+ movie context/++resource++loops.swf;
+ ">
loops
@@ -24,7 +26,7 @@
+
diff --git a/resource.py b/resource.py
index fac4c9f..8279786 100644
--- a/resource.py
+++ b/resource.py
@@ -147,6 +147,7 @@ class Resource(Image, Contained):
def getClients(self, relationships=None):
if relationships is None:
relationships = [TargetRelation]
+ # Versioning: obj = IVersionable(self).master
rels = getRelations(second=self, relationships=relationships)
return [r.first for r in rels]
diff --git a/versioning/README.txt b/versioning/README.txt
new file mode 100644
index 0000000..f0cddca
--- /dev/null
+++ b/versioning/README.txt
@@ -0,0 +1,68 @@
+===============================================================
+loops - Linked Objects for Organization and Processing Services
+===============================================================
+
+Managing versions of resources.
+
+ ($Id$)
+
+
+Setting up a loops Site and Utilities
+=====================================
+
+Let's do some basic set up
+
+ >>> from zope import component, interface
+ >>> from zope.app.testing.setup import placefulSetUp, placefulTearDown
+ >>> site = placefulSetUp(True)
+
+and build a simple loops site with a concept manager and some concepts
+(with a relation registry, a catalog, and all the type machinery - what
+in real life is done via standard ZCML setup or via local utility
+configuration):
+
+ >>> from loops.versioning.testsetup import TestSite
+ >>> t = TestSite(site)
+ >>> concepts, resources, views = t.setup()
+
+ >>> #sorted(concepts)
+ >>> #sorted(resources)
+ >>> len(concepts) + len(resources)
+ 24
+
+
+Version Information
+===================
+
+ >>> from loops.versioning.interfaces import IVersionable
+ >>> from loops.versioning.versionable import VersionableResource
+ >>> component.provideAdapter(VersionableResource)
+
+We can access versioning information for an object by using an IVersionable
+adapter on the object.
+
+ >>> d001 = resources['d001.txt']
+ >>> vD001 = IVersionable(d001)
+
+If there aren't any versions associated with the object we get the default
+values:
+
+ >>> vD001.master is d001
+ True
+ >>> vD001.versionId
+ '1.1'
+ >>> vD001.versions
+ {}
+ >>> vD001.currentVersion is d001
+ True
+ >>> vD001.releasedVersion is d001
+ True
+
+Now we can create a new version for our document:
+
+ >>> d001v1_1 = vD001.createVersion()
+ >>> sorted(resources)
+
+ >>> vD001v1_1 = IVersionable(d001v1_1)
+ >>> vD001v1_1.versionId
+ '1.2'
diff --git a/versioning/__init__.py b/versioning/__init__.py
new file mode 100644
index 0000000..4bc90fb
--- /dev/null
+++ b/versioning/__init__.py
@@ -0,0 +1,4 @@
+"""
+$Id$
+"""
+
diff --git a/versioning/configure.zcml b/versioning/configure.zcml
new file mode 100644
index 0000000..13a2ddd
--- /dev/null
+++ b/versioning/configure.zcml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/versioning/interfaces.py b/versioning/interfaces.py
new file mode 100644
index 0000000..a5080d7
--- /dev/null
+++ b/versioning/interfaces.py
@@ -0,0 +1,70 @@
+#
+# Copyright (c) 2006 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
+#
+
+"""
+Versioning interfaces.
+
+$Id$
+"""
+
+from zope.interface import Interface, Attribute
+from zope import interface, component, schema
+
+
+class IVersionable(Interface):
+ """ An object that may exist in different versions.
+ """
+
+ versionNumbers = Attribute(u'A tuple of version numbers for the context '
+ 'object, with a number for each level')
+
+ variantIds = Attribute(u'A tuple of variant IDs (e.g. for language '
+ 'varuants) for the context object')
+
+ versionId = Attribute(u'A string identifying this version, e.g. 1.1_de, '
+ 'derived from versionNumbers and variantIds')
+
+ master = Attribute(u'The object (master version) that should be used for access to '
+ 'version-independent attributes and central '
+ 'versioning metadata')
+
+ # attributes taken from the master version:
+
+ versions = Attribute(u'A dictionary of all versions of this object')
+
+ currentVersion = Attribute(u'The default version to be used for editing')
+
+ releasedVersion = Attribute(u'The default version to be used for viewing')
+
+ def createVersion(level=1):
+ """ Create a copy of the context object as a new version and return it.
+
+ The level of the version says if it is a minor (1) or major (0)
+ version. (It would even be possible to have more than two levels.
+ """
+
+ def createVariant(id, level=0):
+ """ Create a copy of the context object as a new variant and return it.
+
+ The level provides the position in the variantIds tuple.
+ """
+
+
+class IVersionInfo(Interface):
+ """ Versioning metadata, e.g. criteria for version selection.
+ """
diff --git a/versioning/tests.py b/versioning/tests.py
new file mode 100755
index 0000000..c77b101
--- /dev/null
+++ b/versioning/tests.py
@@ -0,0 +1,23 @@
+# $Id$
+
+import unittest, doctest
+from zope.testing.doctestunit import DocFileSuite
+from zope.interface.verify import verifyClass
+from loops.versioning import versioninfo
+
+class Test(unittest.TestCase):
+ "Basic tests for the expert sub-package."
+
+ def testSomething(self):
+ pass
+
+
+def test_suite():
+ flags = doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS
+ return unittest.TestSuite((
+ unittest.makeSuite(Test),
+ DocFileSuite('README.txt', optionflags=flags),
+ ))
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')
diff --git a/versioning/testsetup.py b/versioning/testsetup.py
new file mode 100644
index 0000000..b33c4b8
--- /dev/null
+++ b/versioning/testsetup.py
@@ -0,0 +1,98 @@
+"""
+Set up a loops site for testing.
+
+$Id$
+"""
+
+from zope import component
+from zope.app.catalog.catalog import Catalog
+from zope.app.catalog.interfaces import ICatalog
+from zope.app.catalog.field import FieldIndex
+from zope.app.catalog.text import TextIndex
+
+from cybertools.relation.tests import IntIdsStub
+from cybertools.relation.registry import RelationRegistry
+from cybertools.relation.interfaces import IRelationRegistry
+from cybertools.relation.registry import IndexableRelationAdapter
+from cybertools.typology.interfaces import IType
+
+from loops import Loops
+from loops import util
+from loops.interfaces import IIndexAttributes
+from loops.concept import Concept
+from loops.concept import IndexAttributes as ConceptIndexAttributes
+from loops.resource import Resource
+from loops.resource import IndexAttributes as ResourceIndexAttributes
+from loops.knowledge.setup import SetupManager as KnowledgeSetupManager
+from loops.setup import SetupManager, addObject
+from loops.type import ConceptType, ResourceType, TypeConcept
+
+
+class TestSite(object):
+
+ def __init__(self, site):
+ self.site = site
+
+ def setup(self):
+ site = self.site
+
+ component.provideUtility(IntIdsStub())
+ relations = RelationRegistry()
+ relations.setupIndexes()
+ component.provideUtility(relations, IRelationRegistry)
+ component.provideAdapter(IndexableRelationAdapter)
+
+ component.provideAdapter(ConceptType)
+ component.provideAdapter(ResourceType)
+ component.provideAdapter(TypeConcept)
+
+ catalog = Catalog()
+ component.provideUtility(catalog, ICatalog)
+
+ catalog['loops_title'] = TextIndex('title', IIndexAttributes, True)
+ catalog['loops_text'] = TextIndex('text', IIndexAttributes, True)
+ catalog['loops_type'] = FieldIndex('tokenForSearch', IType, False)
+
+ loopsRoot = site['loops'] = Loops()
+
+ component.provideAdapter(KnowledgeSetupManager, name='knowledge')
+ setup = SetupManager(loopsRoot)
+ concepts, resources, views = setup.setup()
+
+ component.provideAdapter(ConceptIndexAttributes)
+ component.provideAdapter(ResourceIndexAttributes)
+
+ tType = concepts.getTypeConcept()
+ tDomain = concepts['domain']
+ tTextDocument = concepts['textdocument']
+
+ tCustomer = addObject(concepts, Concept, 'customer', title=u'Customer',
+ type=tType)
+ dProjects = addObject(concepts, Concept, 'projects',
+ title=u'Project Domain', type=tDomain)
+ tCustomer.assignParent(dProjects)
+
+ cust1 = addObject(concepts, Concept, 'cust1',
+ title=u'Customer 1', type=tCustomer)
+ cust2 = addObject(concepts, Concept, 'cust2',
+ title=u'Customer 2', type=tCustomer)
+ cust3 = addObject(concepts, Concept, 'cust3',
+ title=u'Customer 3', type=tCustomer)
+ d001 = addObject(resources, Resource, 'd001.txt',
+ title=u'Doc 001', type=tTextDocument)
+ d001.assignConcept(cust1)
+ d002 = addObject(resources, Resource, 'd002.txt',
+ title=u'Doc 002', type=tTextDocument)
+ d002.assignConcept(cust3)
+ d003 = addObject(resources, Resource, 'd003.txt',
+ title=u'Doc 003', type=tTextDocument)
+ d003.assignConcept(cust1)
+
+ for c in concepts.values():
+ catalog.index_doc(int(util.getUidForObject(c)), c)
+ for r in resources.values():
+ catalog.index_doc(int(util.getUidForObject(r)), r)
+
+ return concepts, resources, views
+
+
diff --git a/versioning/versionable.py b/versioning/versionable.py
new file mode 100644
index 0000000..8a28bc6
--- /dev/null
+++ b/versioning/versionable.py
@@ -0,0 +1,149 @@
+#
+# Copyright (c) 2007 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
+#
+
+"""
+Utilities for managing version informations.
+
+$Id$
+"""
+
+from BTrees.OOBTree import OOBTree
+from zope.component import adapts
+from zope.interface import implements
+from zope.cachedescriptors.property import Lazy
+from zope.traversing.api import getName, getParent
+
+from cybertools.text.mimetypes import extensions
+from cybertools.typology.interfaces import IType
+from loops.interfaces import IResource
+from loops.versioning.interfaces import IVersionable
+
+
+_not_found = object()
+attrPattern = '__version_%s__'
+
+
+class VersionableResource(object):
+ """ An adapter that enables a resource to store version information.
+ """
+
+ implements(IVersionable)
+ adapts(IResource)
+
+ def __init__(self, context):
+ self.context = context
+
+ def getVersioningAttribute(self, attr, default):
+ attrName = attrPattern % attr
+ value = getattr(self.context, attrName, _not_found)
+ if value is _not_found:
+ return default
+ return value
+
+ def initVersioningAttribute(self, attr, value):
+ attrName = attrPattern % attr
+ value = getattr(self.context, attrName, _not_found)
+ if value is _not_found:
+ setattr(self.context, attrName, value)
+
+ def setVersioningAttribute(self, attr, value):
+ attrName = attrPattern % attr
+ setattr(self.context, attrName, value)
+
+ @Lazy
+ def versionNumbers(self):
+ return self.getVersioningAttribute('versionNumbers', (1, 1))
+
+ @Lazy
+ def variantIds(self):
+ return self.getVersioningAttribute('variantIds', ())
+
+ @Lazy
+ def versionId(self):
+ versionPart = '.'.join(str(n) for n in self.versionNumbers)
+ return '_'.join([versionPart] + list(self.variantIds))
+
+ @Lazy
+ def master(self):
+ return self.getVersioningAttribute('master', self.context)
+
+ @Lazy
+ def versionableMaster(self):
+ """ The adapted master... """
+ return IVersionable(self.master)
+
+ @property
+ def versions(self):
+ return self.versionableMaster.getVersioningAttribute('versions', {})
+
+ @property
+ def currentVersion(self):
+ return self.versionableMaster.getVersioningAttribute('currentVersion', self.master)
+
+ @property
+ def releasedVersion(self):
+ m = self.versionableMaster
+ return self.versionableMaster.getVersioningAttribute('releasedVersion', self.master)
+
+ def createVersion(self, level=1):
+ context = self.context
+ versionableMaster = self.versionableMaster
+ # get the new version numbers
+ vn = list(IVersionable(self.currentVersion).versionNumbers)
+ while len(vn) <= level:
+ vn.append(1)
+ vn[level] += 1
+ # create new object
+ cls = context.__class__
+ obj = cls()
+ # set versioning attributes of new object
+ versionableObj = IVersionable(obj)
+ versionableObj.setVersioningAttribute('versionNumbers', tuple(vn))
+ versionableObj.setVersioningAttribute('variantIds', self.variantIds)
+ versionableObj.setVersioningAttribute('master', self.master)
+ # generate name for new object, register in parent
+ versionId = versionableObj.versionId
+ name = self.generateName(getName(context),
+ extensions.get(context.contentType, ''),
+ versionId)
+ getParent(context)[name] = obj
+ # set resource attributes
+ obj.resourceType = context.resourceType
+ ti = IType(context).typeInterface
+ if ti is not None:
+ adaptedContext = ti(context)
+ adaptedObj = ti(obj)
+ for attr in ti:
+ if attr not in ('resourceType',):
+ setattr(adaptedObj, attr, getattr(adaptedContext, attr))
+ # set attributes of the master version
+ versionableMaster.initVersioningAttribute('versions', OOBTree())
+ self.versions[versionId] = obj
+ versionableMaster.setVersioningAttribute('currentVersion', obj)
+ return obj
+
+ def generateName(self, name, ext, versionId):
+ if ext:
+ ext = '.' + ext
+ if ext and name.endswith(ext):
+ name = name[:-len(ext)]
+ elif len(name) > 3 and name[-4] == '.':
+ ext = name[-4:]
+ name = name[:-4]
+ return name + '_' + versionId + ext
+
diff --git a/versioning/versioninfo.py b/versioning/versioninfo.py
new file mode 100644
index 0000000..a354a84
--- /dev/null
+++ b/versioning/versioninfo.py
@@ -0,0 +1,44 @@
+#
+# Copyright (c) 2007 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
+#
+
+"""
+Utilities for managing version informations.
+
+$Id$
+"""
+
+from zope.interface import implements
+
+from loops.versioning.interfaces import IVersionInfo
+
+
+class VersionInfo(object):
+ """ Collects and provides informations related to object versions.
+ """
+
+ implements(IVersionInfo)
+
+
+def getVersionInfo(obj, request):
+ """ Check if a special version should be used for the object
+ provided.
+
+ In addition return meta information about the object versions
+ so that this will not have to be retrieved later.
+ """
+ return obj, VersionInfo()
diff --git a/view.py b/view.py
index 3f68c0c..7872168 100644
--- a/view.py
+++ b/view.py
@@ -40,11 +40,12 @@ from cybertools.relation import DyadicRelation
from cybertools.relation.registry import getRelations
from cybertools.relation.interfaces import IRelationRegistry, IRelatable
-from interfaces import IView, INode
-from interfaces import IViewManager, INodeContained
-from interfaces import ILoopsContained
-from interfaces import ITargetRelation
-from interfaces import IConcept
+from loops.interfaces import IView, INode
+from loops.interfaces import IViewManager, INodeContained
+from loops.interfaces import ILoopsContained
+from loops.interfaces import ITargetRelation
+from loops.interfaces import IConcept
+from loops.versioning.versioninfo import getVersionInfo
class View(object):
@@ -202,6 +203,8 @@ class NodeTraverser(ItemTraverser):
else:
target = self.context.target
if target is not None:
+ # provide versioning info and switch to correct version if appropriate
+ target, versionInfo = getVersionInfo(target, request)
# remember self.context in request
viewAnnotations = request.annotations.setdefault('loops.view', {})
viewAnnotations['node'] = self.context
@@ -211,6 +214,7 @@ class NodeTraverser(ItemTraverser):
else:
# we'll use the target object in the node's context
viewAnnotations['target'] = target
+ viewAnnotations['versionInfo'] = versionInfo
return self.context
return super(NodeTraverser, self).publishTraverse(request, name)