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)