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:
parent
c35d7fab4e
commit
a66d7f81c7
12 changed files with 238 additions and 94 deletions
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
40
resource.py
40
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
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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.
|
||||
"""
|
||||
|
|
|
@ -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
57
versioning/util.py
Normal 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
|
||||
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
7
view.py
7
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)
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue