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

View file

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

View file

@ -5,6 +5,8 @@
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="form.action" value="edit"
tal:attributes="value view/form_action" />
<input type="hidden" name="version"
tal:attributes="value request/version | nothing" />
<table cellpadding="3" class="form">
<tbody><tr><th colspan="5"><br />
<span tal:replace="view/title"
@ -24,6 +26,7 @@
</tr>
<tr metal:use-macro="view/template/macros/assignments" />
<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" />
</tbody>
</table>
@ -165,6 +168,33 @@
</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">
<td colspan="5"
tal:define="dlgName view/dialog_name">

View file

@ -226,7 +226,9 @@
<div tal:condition="view/hasEditableTarget">
<a href="#"
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...
</a>
</div>

View file

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

View file

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

View file

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

View file

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

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 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',):
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):

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