more on versioning: basically working

git-svn-id: svn://svn.cy55.de/Zope3/src/loops/trunk@1655 fd906abe-77d9-0310-91a1-e0d9ade77398
This commit is contained in:
helmutm 2007-03-20 12:44:19 +00:00
parent c35d7fab4e
commit a66d7f81c7
12 changed files with 238 additions and 94 deletions

View file

@ -51,6 +51,7 @@ from loops.resource import Resource
from loops.type import ITypeConcept from loops.type import ITypeConcept
from loops import util from loops import util
from loops.util import _ from loops.util import _
from loops.versioning.interfaces import IVersionable
class NameField(schema.ASCIILine): class NameField(schema.ASCIILine):
@ -237,6 +238,22 @@ class BaseView(GenericView):
return util.KeywordVocabulary(general return util.KeywordVocabulary(general
+ self.listTypesForSearch(('resource',), ('system', 'hidden'),)) + 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 # controlling editing
@Lazy @Lazy

View file

@ -51,6 +51,7 @@ from loops.resource import Resource
from loops.type import ITypeConcept from loops.type import ITypeConcept
from loops import util from loops import util
from loops.util import _ from loops.util import _
from loops.versioning.interfaces import IVersionable
# special widgets # special widgets
@ -248,7 +249,14 @@ class EditObject(FormController):
selected = None selected = None
def update(self): 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 return True
@Lazy @Lazy
@ -314,6 +322,14 @@ class EditObject(FormController):
if not exists: if not exists:
obj.assignConcept(concept, predicate) 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): class CreateObject(EditObject):

View file

@ -5,6 +5,8 @@
<form method="post" enctype="multipart/form-data"> <form method="post" enctype="multipart/form-data">
<input type="hidden" name="form.action" value="edit" <input type="hidden" name="form.action" value="edit"
tal:attributes="value view/form_action" /> tal:attributes="value view/form_action" />
<input type="hidden" name="version"
tal:attributes="value request/version | nothing" />
<table cellpadding="3" class="form"> <table cellpadding="3" class="form">
<tbody><tr><th colspan="5"><br /> <tbody><tr><th colspan="5"><br />
<span tal:replace="view/title" <span tal:replace="view/title"
@ -24,6 +26,7 @@
</tr> </tr>
<tr metal:use-macro="view/template/macros/assignments" /> <tr metal:use-macro="view/template/macros/assignments" />
<tr metal:use-macro="view/template/macros/search_concepts" /> <tr metal:use-macro="view/template/macros/search_concepts" />
<tr metal:use-macro="view/template/macros/versioning" />
<tr metal:use-macro="view/template/macros/buttons" /> <tr metal:use-macro="view/template/macros/buttons" />
</tbody> </tbody>
</table> </table>
@ -165,6 +168,33 @@
</tr> </tr>
<metal:versioning define-macro="versioning"
tal:define="versionInfo view/versionInfo"
tal:condition="versionInfo">
<tr>
<td colspan="5" class="headline">Versioning</td>
</tr>
<tr>
<td colspan="2">
Version:
<span tal:content="versionInfo">1.1 (current, released)</span>
</td>
<td title="Select if you want to create a new version">
<input type="checkbox"
name="version.create" id="version.create"
value="create" />
<label for="version.create">New version:</label>
</td>
<td colspan="2">
<select name="version.level">
<option value="1">minor</option>
<option value="0">major</option>
</select>
</td>
</tr>
</metal:versioning>
<tr metal:define-macro="buttons"> <tr metal:define-macro="buttons">
<td colspan="5" <td colspan="5"
tal:define="dlgName view/dialog_name"> tal:define="dlgName view/dialog_name">

View file

@ -226,7 +226,9 @@
<div tal:condition="view/hasEditableTarget"> <div tal:condition="view/hasEditableTarget">
<a href="#" <a href="#"
onclick="objectDialog('edit', 'edit_object.html'); return false;" onclick="objectDialog('edit', 'edit_object.html'); return false;"
tal:attributes="onclick string:objectDialog('edit', '$url/edit_object.html');; return false;;"> 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... Edit Resource...
</a> </a>
</div> </div>

View file

@ -47,16 +47,17 @@ from cybertools.storage.interfaces import IExternalStorage
from cybertools.text.interfaces import ITextTransform from cybertools.text.interfaces import ITextTransform
from cybertools.typology.interfaces import IType, ITypeManager from cybertools.typology.interfaces import IType, ITypeManager
from interfaces import IBaseResource, IResource from loops.interfaces import IBaseResource, IResource
from interfaces import IFile, IExternalFile, INote from loops.interfaces import IFile, IExternalFile, INote
from interfaces import IDocument, ITextDocument, IDocumentSchema, IDocumentView from loops.interfaces import IDocument, ITextDocument, IDocumentSchema, IDocumentView
from interfaces import IMediaAsset, IMediaAssetView from loops.interfaces import IMediaAsset, IMediaAssetView
from interfaces import IResourceManager, IResourceManagerContained from loops.interfaces import IResourceManager, IResourceManagerContained
from interfaces import ILoopsContained from loops.interfaces import ILoopsContained
from interfaces import IIndexAttributes from loops.interfaces import IIndexAttributes
from concept import ResourceRelation from loops.concept import ResourceRelation
from common import ResourceAdapterBase from loops.common import ResourceAdapterBase
from view import TargetRelation from loops.versioning.util import getMaster
from loops.view import TargetRelation
_ = MessageFactory('loops') _ = MessageFactory('loops')
@ -147,26 +148,31 @@ class Resource(Image, Contained):
def getClients(self, relationships=None): def getClients(self, relationships=None):
if relationships is None: if relationships is None:
relationships = [TargetRelation] relationships = [TargetRelation]
# Versioning: obj = IVersionable(self).master obj = getMaster(self) # use the master version for relations
rels = getRelations(second=self, relationships=relationships) rels = getRelations(second=obj, relationships=relationships)
return [r.first for r in rels] return [r.first for r in rels]
# concept relations # concept relations
# note: we always use the master version for relations, see getMaster()
def getConceptRelations (self, predicates=None, concept=None): def getConceptRelations (self, predicates=None, concept=None):
predicates = predicates is None and ['*'] or predicates 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... # TODO: sort...
return getRelations(first=concept, second=self, relationships=relationships) return getRelations(first=concept, second=obj, relationships=relationships)
def getConcepts(self, predicates=None): 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): def assignConcept(self, concept, predicate=None):
concept.assignResource(self, predicate) obj = getMaster(self)
concept.assignResource(obj, predicate)
def deassignConcept(self, concept, predicates=None): def deassignConcept(self, concept, predicates=None):
concept.deassignResource(self, predicates) obj = getMaster(self)
concept.deassignResource(obj, predicates)
# ISized interface # ISized interface

View file

@ -13,6 +13,7 @@ Setting up a loops Site and Utilities
Let's do some basic set up Let's do some basic set up
>>> from zope import component, interface >>> from zope import component, interface
>>> from zope.traversing.api import getName
>>> from zope.app.testing.setup import placefulSetUp, placefulTearDown >>> from zope.app.testing.setup import placefulSetUp, placefulTearDown
>>> site = placefulSetUp(True) >>> site = placefulSetUp(True)
@ -42,6 +43,8 @@ We can access versioning information for an object by using an IVersionable
adapter on the object. adapter on the object.
>>> d001 = resources['d001.txt'] >>> d001 = resources['d001.txt']
>>> d001.title
u'Doc 001'
>>> vD001 = IVersionable(d001) >>> vD001 = IVersionable(d001)
If there aren't any versions associated with the object we get the default If there aren't any versions associated with the object we get the default
@ -55,14 +58,70 @@ values:
{} {}
>>> vD001.currentVersion is d001 >>> vD001.currentVersion is d001
True True
>>> vD001.releasedVersion is d001 >>> vD001.releasedVersion is None
True True
Now we can create a new version for our document: Now we can create a new version for our document:
>>> d001v1_1 = vD001.createVersion() >>> d001v1_2 = vD001.createVersion()
>>> sorted(resources) >>> getName(d001v1_2)
u'd001_1.2.txt'
>>> d001v1_2.title
u'Doc 001'
>>> vD001v1_1 = IVersionable(d001v1_1) >>> vD001v1_2 = IVersionable(d001v1_2)
>>> vD001v1_1.versionId >>> vD001v1_2.versionId
'1.2' '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'

View file

@ -64,7 +64,3 @@ class IVersionable(Interface):
The level provides the position in the variantIds tuple. The level provides the position in the variantIds tuple.
""" """
class IVersionInfo(Interface):
""" Versioning metadata, e.g. criteria for version selection.
"""

View file

@ -3,7 +3,7 @@
import unittest, doctest import unittest, doctest
from zope.testing.doctestunit import DocFileSuite from zope.testing.doctestunit import DocFileSuite
from zope.interface.verify import verifyClass from zope.interface.verify import verifyClass
from loops.versioning import versioninfo from loops.versioning import versionable
class Test(unittest.TestCase): class Test(unittest.TestCase):
"Basic tests for the expert sub-package." "Basic tests for the expert sub-package."

57
versioning/util.py Normal file
View file

@ -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

View file

@ -24,8 +24,9 @@ $Id$
from BTrees.OOBTree import OOBTree from BTrees.OOBTree import OOBTree
from zope.component import adapts from zope.component import adapts
from zope.interface import implements from zope.interface import implements, Attribute
from zope.cachedescriptors.property import Lazy from zope.cachedescriptors.property import Lazy
from zope.schema.interfaces import IField
from zope.traversing.api import getName, getParent from zope.traversing.api import getName, getParent
from cybertools.text.mimetypes import extensions from cybertools.text.mimetypes import extensions
@ -55,16 +56,19 @@ class VersionableResource(object):
return default return default
return value 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): def setVersioningAttribute(self, attr, value):
attrName = attrPattern % attr attrName = attrPattern % attr
setattr(self.context, attrName, value) 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 @Lazy
def versionNumbers(self): def versionNumbers(self):
return self.getVersioningAttribute('versionNumbers', (1, 1)) return self.getVersioningAttribute('versionNumbers', (1, 1))
@ -98,7 +102,7 @@ class VersionableResource(object):
@property @property
def releasedVersion(self): def releasedVersion(self):
m = self.versionableMaster m = self.versionableMaster
return self.versionableMaster.getVersioningAttribute('releasedVersion', self.master) return self.versionableMaster.getVersioningAttribute('releasedVersion', None)
def createVersion(self, level=1): def createVersion(self, level=1):
context = self.context context = self.context
@ -108,6 +112,9 @@ class VersionableResource(object):
while len(vn) <= level: while len(vn) <= level:
vn.append(1) vn.append(1)
vn[level] += 1 vn[level] += 1
for l in range(level+1, len(vn)):
# reset lower levels
vn[l] = 1
# create new object # create new object
cls = context.__class__ cls = context.__class__
obj = cls() obj = cls()
@ -123,18 +130,17 @@ class VersionableResource(object):
versionId) versionId)
getParent(context)[name] = obj getParent(context)[name] = obj
# set resource attributes # set resource attributes
obj.resourceType = context.resourceType
ti = IType(context).typeInterface ti = IType(context).typeInterface
if ti is not None: attrs = set((ti and list(ti) or [])
adaptedContext = ti(context) + ['title', 'description', 'data', 'contentType'])
adaptedObj = ti(obj) adaptedContext = ti and ti(context) or context
for attr in ti: adaptedObj = ti and ti(obj) or obj
if attr not in ('resourceType',): for attr in attrs:
setattr(adaptedObj, attr, getattr(adaptedContext, attr)) setattr(adaptedObj, attr, getattr(adaptedContext, attr))
# set attributes of the master version # set attributes of the master version
versionableMaster.initVersioningAttribute('versions', OOBTree())
self.versions[versionId] = obj
versionableMaster.setVersioningAttribute('currentVersion', obj) versionableMaster.setVersioningAttribute('currentVersion', obj)
versionableMaster.initVersions()
self.versions[versionId] = obj
return obj return obj
def generateName(self, name, ext, versionId): def generateName(self, name, ext, versionId):

View file

@ -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()

View file

@ -45,7 +45,7 @@ from loops.interfaces import IViewManager, INodeContained
from loops.interfaces import ILoopsContained from loops.interfaces import ILoopsContained
from loops.interfaces import ITargetRelation from loops.interfaces import ITargetRelation
from loops.interfaces import IConcept from loops.interfaces import IConcept
from loops.versioning.versioninfo import getVersionInfo from loops.versioning.util import getVersion
class View(object): class View(object):
@ -203,8 +203,6 @@ class NodeTraverser(ItemTraverser):
else: else:
target = self.context.target target = self.context.target
if target is not None: 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 # remember self.context in request
viewAnnotations = request.annotations.setdefault('loops.view', {}) viewAnnotations = request.annotations.setdefault('loops.view', {})
viewAnnotations['node'] = self.context viewAnnotations['node'] = self.context
@ -212,9 +210,10 @@ class NodeTraverser(ItemTraverser):
# we have to use the target object directly # we have to use the target object directly
return target return target
else: else:
# switch to correct version if appropriate
target = getVersion(target, request)
# we'll use the target object in the node's context # we'll use the target object in the node's context
viewAnnotations['target'] = target viewAnnotations['target'] = target
viewAnnotations['versionInfo'] = versionInfo
return self.context return self.context
return super(NodeTraverser, self).publishTraverse(request, name) return super(NodeTraverser, self).publishTraverse(request, name)