diff --git a/browser/common.py b/browser/common.py index b329959..ae47666 100644 --- a/browser/common.py +++ b/browser/common.py @@ -51,6 +51,7 @@ from loops.resource import Resource from loops.type import ITypeConcept from loops import util from loops.util import _ +from loops.versioning.interfaces import IVersionable class NameField(schema.ASCIILine): @@ -237,6 +238,22 @@ class BaseView(GenericView): return util.KeywordVocabulary(general + self.listTypesForSearch(('resource',), ('system', 'hidden'),)) + # versioning + + @Lazy + def versionInfo(self): + context = self.context + versionable = IVersionable(context, None) + if versionable is None: + return '' + versionId = versionable.versionId + current = (versionable.currentVersion == context) and 'current' or '' + released = (versionable.releasedVersion == context) and 'released' or '' + if not current and not released: + return versionId + addInfo = ', '.join(e for e in (current, released) if e) + return '%s (%s)' % (versionId, addInfo) + # controlling editing @Lazy diff --git a/browser/form.py b/browser/form.py index 10ad5f1..93bf1fd 100644 --- a/browser/form.py +++ b/browser/form.py @@ -51,6 +51,7 @@ from loops.resource import Resource from loops.type import ITypeConcept from loops import util from loops.util import _ +from loops.versioning.interfaces import IVersionable # special widgets @@ -248,7 +249,14 @@ class EditObject(FormController): selected = None def update(self): - self.updateFields(self.view.virtualTargetObject) + # create new version if necessary + target = self.view.virtualTargetObject + obj = self.checkCreateVersion(target) + if obj != target: + # make sure new version is used by the view + self.view.virtualTargetObject = obj + self.request.annotations['loops.view']['target'] = obj + self.updateFields(obj) return True @Lazy @@ -314,6 +322,14 @@ class EditObject(FormController): if not exists: obj.assignConcept(concept, predicate) + def checkCreateVersion(self, obj): + form = self.request.form + if form.get('version.create'): + versionable = IVersionable(obj) + level = int(form.get('version.level', 1)) + return versionable.createVersion(level) + return obj + class CreateObject(EditObject): diff --git a/browser/form_macros.pt b/browser/form_macros.pt index b05db60..49b0bcc 100644 --- a/browser/form_macros.pt +++ b/browser/form_macros.pt @@ -5,6 +5,8 @@
+ +

@@ -165,6 +168,33 @@ + + + Versioning + + + + Version: + 1.1 (current, released) + + + + + + + + + + + + diff --git a/browser/node_macros.pt b/browser/node_macros.pt index 46b1d5b..4a422d9 100644 --- a/browser/node_macros.pt +++ b/browser/node_macros.pt @@ -226,7 +226,9 @@
+ tal:define="version request/version|nothing; + versionPar python: version and '?version=$version' or ''" + tal:attributes="onclick string:objectDialog('edit', '$url/edit_object.html$versionPar');; return false;;"> Edit Resource...
diff --git a/resource.py b/resource.py index 8279786..89454da 100644 --- a/resource.py +++ b/resource.py @@ -47,16 +47,17 @@ from cybertools.storage.interfaces import IExternalStorage from cybertools.text.interfaces import ITextTransform from cybertools.typology.interfaces import IType, ITypeManager -from interfaces import IBaseResource, IResource -from interfaces import IFile, IExternalFile, INote -from interfaces import IDocument, ITextDocument, IDocumentSchema, IDocumentView -from interfaces import IMediaAsset, IMediaAssetView -from interfaces import IResourceManager, IResourceManagerContained -from interfaces import ILoopsContained -from interfaces import IIndexAttributes -from concept import ResourceRelation -from common import ResourceAdapterBase -from view import TargetRelation +from loops.interfaces import IBaseResource, IResource +from loops.interfaces import IFile, IExternalFile, INote +from loops.interfaces import IDocument, ITextDocument, IDocumentSchema, IDocumentView +from loops.interfaces import IMediaAsset, IMediaAssetView +from loops.interfaces import IResourceManager, IResourceManagerContained +from loops.interfaces import ILoopsContained +from loops.interfaces import IIndexAttributes +from loops.concept import ResourceRelation +from loops.common import ResourceAdapterBase +from loops.versioning.util import getMaster +from loops.view import TargetRelation _ = MessageFactory('loops') @@ -147,26 +148,31 @@ 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) + obj = getMaster(self) # use the master version for relations + rels = getRelations(second=obj, relationships=relationships) return [r.first for r in rels] # concept relations + # note: we always use the master version for relations, see getMaster() def getConceptRelations (self, predicates=None, concept=None): predicates = predicates is None and ['*'] or predicates - relationships = [ResourceRelation(None, self, p) for p in predicates] + obj = getMaster(self) + relationships = [ResourceRelation(None, obj, p) for p in predicates] # TODO: sort... - return getRelations(first=concept, second=self, relationships=relationships) + return getRelations(first=concept, second=obj, relationships=relationships) def getConcepts(self, predicates=None): - return [r.first for r in self.getConceptRelations(predicates)] + obj = getMaster(self) + return [r.first for r in obj.getConceptRelations(predicates)] def assignConcept(self, concept, predicate=None): - concept.assignResource(self, predicate) + obj = getMaster(self) + concept.assignResource(obj, predicate) def deassignConcept(self, concept, predicates=None): - concept.deassignResource(self, predicates) + obj = getMaster(self) + concept.deassignResource(obj, predicates) # ISized interface diff --git a/versioning/README.txt b/versioning/README.txt index f0cddca..01c7514 100644 --- a/versioning/README.txt +++ b/versioning/README.txt @@ -13,6 +13,7 @@ Setting up a loops Site and Utilities Let's do some basic set up >>> from zope import component, interface + >>> from zope.traversing.api import getName >>> from zope.app.testing.setup import placefulSetUp, placefulTearDown >>> site = placefulSetUp(True) @@ -42,6 +43,8 @@ We can access versioning information for an object by using an IVersionable adapter on the object. >>> d001 = resources['d001.txt'] + >>> d001.title + u'Doc 001' >>> vD001 = IVersionable(d001) If there aren't any versions associated with the object we get the default @@ -55,14 +58,70 @@ values: {} >>> vD001.currentVersion is d001 True - >>> vD001.releasedVersion is d001 + >>> vD001.releasedVersion is None True Now we can create a new version for our document: - >>> d001v1_1 = vD001.createVersion() - >>> sorted(resources) + >>> d001v1_2 = vD001.createVersion() + >>> getName(d001v1_2) + u'd001_1.2.txt' + >>> d001v1_2.title + u'Doc 001' - >>> vD001v1_1 = IVersionable(d001v1_1) - >>> vD001v1_1.versionId + >>> vD001v1_2 = IVersionable(d001v1_2) + >>> vD001v1_2.versionId '1.2' + + >>> vD001.currentVersion is d001v1_2 + True + >>> vD001.master is d001 + True + >>> vD001v1_2.master is d001 + True + + >>> sorted(vD001.versions) + ['1.1', '1.2'] + +When we use a higer level (i.e. a lower number for level) to denote +a major version change, the lower levels are reset to 1: + + >>> d001v2_1 = vD001.createVersion(0) + >>> getName(d001v2_1) + u'd001_2.1.txt' + + +Providing the Correct Version +============================= + +When accessing resources as targets for view nodes, the node's traversal adapter +(see loops.view.NodeTraverser) uses the versioning framework to retrieve +the correct version of a resource by calling the getVersion() function. + + >>> from loops.versioning.util import getVersion + >>> from zope.publisher.browser import TestRequest + +The default version is always the released or - if this is not available - +the current version (i.e. the version created most recently): + + >>> IVersionable(getVersion(d001, TestRequest())).versionId + '2.1' + + >>> IVersionable(getVersion(d001v1_2, TestRequest())).versionId + '2.1' + + >>> d002 = resources['d002.txt'] + >>> IVersionable(getVersion(d002, TestRequest())).versionId + '1.1' + +When using the expression "version=this" as a URL parameter the object +addressed will be returned without looking for a special version: + + >>> IVersionable(getVersion(d001, TestRequest(form=dict(version='this')))).versionId + '1.1' + +In addition it is possible to explicitly retrieve a certain version: + + >>> IVersionable(getVersion(d001v1_2, TestRequest(form=dict(version='1.1')))).versionId + '1.1' + diff --git a/versioning/interfaces.py b/versioning/interfaces.py index a5080d7..0011c12 100644 --- a/versioning/interfaces.py +++ b/versioning/interfaces.py @@ -64,7 +64,3 @@ class IVersionable(Interface): 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 index c77b101..198640f 100755 --- a/versioning/tests.py +++ b/versioning/tests.py @@ -3,7 +3,7 @@ import unittest, doctest from zope.testing.doctestunit import DocFileSuite from zope.interface.verify import verifyClass -from loops.versioning import versioninfo +from loops.versioning import versionable class Test(unittest.TestCase): "Basic tests for the expert sub-package." diff --git a/versioning/util.py b/versioning/util.py new file mode 100644 index 0000000..61aad46 --- /dev/null +++ b/versioning/util.py @@ -0,0 +1,57 @@ +# +# 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 loops.versioning.interfaces import IVersionable + + +def getVersion(obj, request): + """ Check if another version should be used for the object + provided and return it. + """ + versionRequest = request.form.get('version') + if versionRequest == 'this': + # we really want this object, not another version + return obj + versionable = IVersionable(obj, None) + if versionable is None: + return obj + if not versionRequest: + # find and return a standard version + v = versionable.releasedVersion + if v is None: + v = versionable.currentVersion + return v + # we might have a versionId in the request + v = versionable.versions.get(versionRequest) + if v is not None: + return v + return obj + + +def getMaster(obj): + versionable = IVersionable(obj, None) + if versionable is None: + return obj + return versionable.master + diff --git a/versioning/versionable.py b/versioning/versionable.py index 8a28bc6..181fa9f 100644 --- a/versioning/versionable.py +++ b/versioning/versionable.py @@ -24,8 +24,9 @@ $Id$ from BTrees.OOBTree import OOBTree from zope.component import adapts -from zope.interface import implements +from zope.interface import implements, Attribute from zope.cachedescriptors.property import Lazy +from zope.schema.interfaces import IField from zope.traversing.api import getName, getParent from cybertools.text.mimetypes import extensions @@ -55,16 +56,19 @@ class VersionableResource(object): 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) + def initVersions(self): + attrName = attrPattern % 'versions' + value = getattr(self.context, attrName, _not_found) + if value is _not_found: + versions = OOBTree() + versions['1.1'] = self.context + setattr(self.context, attrName, versions) + #self.versions['1.1'] = self.context + @Lazy def versionNumbers(self): return self.getVersioningAttribute('versionNumbers', (1, 1)) @@ -98,7 +102,7 @@ class VersionableResource(object): @property def releasedVersion(self): m = self.versionableMaster - return self.versionableMaster.getVersioningAttribute('releasedVersion', self.master) + return self.versionableMaster.getVersioningAttribute('releasedVersion', None) def createVersion(self, level=1): context = self.context @@ -108,6 +112,9 @@ class VersionableResource(object): while len(vn) <= level: vn.append(1) vn[level] += 1 + for l in range(level+1, len(vn)): + # reset lower levels + vn[l] = 1 # create new object cls = context.__class__ obj = cls() @@ -123,18 +130,17 @@ class VersionableResource(object): 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)) + attrs = set((ti and list(ti) or []) + + ['title', 'description', 'data', 'contentType']) + adaptedContext = ti and ti(context) or context + adaptedObj = ti and ti(obj) or obj + for attr in attrs: + setattr(adaptedObj, attr, getattr(adaptedContext, attr)) # set attributes of the master version - versionableMaster.initVersioningAttribute('versions', OOBTree()) - self.versions[versionId] = obj versionableMaster.setVersioningAttribute('currentVersion', obj) + versionableMaster.initVersions() + self.versions[versionId] = obj return obj def generateName(self, name, ext, versionId): diff --git a/versioning/versioninfo.py b/versioning/versioninfo.py deleted file mode 100644 index a354a84..0000000 --- a/versioning/versioninfo.py +++ /dev/null @@ -1,44 +0,0 @@ -# -# 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 7872168..e49e116 100644 --- a/view.py +++ b/view.py @@ -45,7 +45,7 @@ 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 +from loops.versioning.util import getVersion class View(object): @@ -203,8 +203,6 @@ 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 @@ -212,9 +210,10 @@ class NodeTraverser(ItemTraverser): # we have to use the target object directly return target else: + # switch to correct version if appropriate + target = getVersion(target, request) # 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)