From c35d7fab4e5d1e71082eb5cf8edaae19898ab88e Mon Sep 17 00:00:00 2001 From: helmutm Date: Mon, 19 Mar 2007 15:49:56 +0000 Subject: [PATCH] provide basic versioning API for resources git-svn-id: svn://svn.cy55.de/Zope3/src/loops/trunk@1654 fd906abe-77d9-0310-91a1-e0d9ade77398 --- browser/flash/configure.zcml | 2 + browser/flash/flash.pt | 6 +- browser/flash/loops_logo.jpg | Bin 0 -> 1769 bytes configure.zcml | 1 + resource.py | 1 + versioning/README.txt | 68 ++++++++++++++++ versioning/__init__.py | 4 + versioning/configure.zcml | 19 +++++ versioning/interfaces.py | 70 ++++++++++++++++ versioning/tests.py | 23 ++++++ versioning/testsetup.py | 98 +++++++++++++++++++++++ versioning/versionable.py | 149 +++++++++++++++++++++++++++++++++++ versioning/versioninfo.py | 44 +++++++++++ view.py | 14 ++-- 14 files changed, 492 insertions(+), 7 deletions(-) create mode 100644 browser/flash/loops_logo.jpg create mode 100644 versioning/README.txt create mode 100644 versioning/__init__.py create mode 100644 versioning/configure.zcml create mode 100644 versioning/interfaces.py create mode 100755 versioning/tests.py create mode 100644 versioning/testsetup.py create mode 100644 versioning/versionable.py create mode 100644 versioning/versioninfo.py 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 @@ IH;#ol1DK_*X0JymU`Tzh(NlF4SA8-JWAOe8^k&=c)B2j2rDYOLgGBVP#7#wGNl)bCjeOxCdmk5fRn(dkjnKpOMRYCN%+XpCXI?wyYP&YsJoMRIX>>p^&*BnteD}YE;Kq)deS{rP^>l;dtuYWT z)ZK*EpzrU=D^j`=L9SgyNq0qE|7ku^CwwR6vl)s@e)JdjDhRpw7~fq=tn*@`(y z)9B&&c-Du3s`wOCCyBXu{&Dcb_UY7BducV$X8xrLnc}?@aCa}gdRcyZxITY5S|e>S zXdwA%cw7K>j@qh5N}9o}+g?`W6xNScE#2;SVuJTvU&JQ4M16(3n=wuPK4;|TWGmTl zhu+xCUksuVzDCC=5|Eo; zos-adYKV5mT=6R$t06*mo0rVzw=lKT*8hA}2osnf(ft`T1U)OLpUw#qtR;rU##>wt0h&Gq^B zB!!1F6KSHn1s`@$zZlSsY?!E|erFMpNBJlN`Dq8PcFZb6#|~Phb8uMSPk2U@*MI(thF?wlYrLuxoo2&ZCvG$J0TGy+;ECYOXFP+{rg!CEmvn_WQpcmVJ%|V7YzDtu;ZbTvpb|^` zN>!@a9GL>g{+c2PmVK$YbN-t#N&h|vND2*4aAJ=Y!tiO z!>oj!HMrUv6Y4I*i3=j1Xu(}ttGeXX8OE;=*`-luPi2+r(0kf#u0n?4B)`7Hq0M#a zw^Y^aedue>;e{Q+&0*F}Ow--sF~(jh_`|y#Y>CL$KYnF59zCA;bhzBzjF!Amc0q9E zNPq4$)T7%)B~#ZJdYExE}}bkhmR0@myB*WGTCz%^qktWT*xle7pvVu zUl;vhtHSuen8s_;s{2!Qf9t%52Ej(Ij+ehVzS7oT=pXzIHs~|&XO5O5E<5cSP_%ph zs2eHX_m^%;KCLP8mdWji*nC!_YBkk*NJUy}D}wU7$A;7rz2-3sUYoa+RIzT+BZn5& z>V<^3ABy<)$)#nFep131X;Bn4j153d<7J$g$K<}A)+!Vfj*C`oBUr}Sk=dma7O&S% zg*GUs)<0R-3uMH-!=-GE=}cM-RiBB$iL$lEs^4sz%KSJvn<2WGz`)Y~v`C}!%ak9D z78uM3)C`+emZL4Iw{(d_*-V}8u=hn$vNac;-T zi^mTdefa%O`YrE1K}nBI(N%uUBQjK`T2ZFD?R1(kU^wN{OyegS-GOqnYst{}5c|Gj z* + 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)