provide basic versioning API for resources
git-svn-id: svn://svn.cy55.de/Zope3/src/loops/trunk@1654 fd906abe-77d9-0310-91a1-e0d9ade77398
This commit is contained in:
parent
53810c1a19
commit
c35d7fab4e
14 changed files with 492 additions and 7 deletions
|
@ -5,6 +5,8 @@
|
|||
xmlns="http://namespaces.zope.org/browser"
|
||||
i18n_domain="zope">
|
||||
|
||||
<resource name="loops_logo.jpg" file="loops_logo.jpg" />
|
||||
|
||||
<!-- Flash user interface -->
|
||||
|
||||
<page
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<html tal:define="dummy view/setHeaders;
|
||||
hostURL view/loopsUrl;
|
||||
movie context/++resource++loops.swf">
|
||||
logoURL context/++resource++loops_logo.jpg;
|
||||
movie context/++resource++loops.swf;
|
||||
">
|
||||
|
||||
<head>
|
||||
<title>loops</title>
|
||||
|
@ -24,7 +26,7 @@
|
|||
<param name=quality value=high>
|
||||
<param name="wmode" value="opaque">
|
||||
<embed src="loops.swf?hostURL=http://z3.loops.cy55.de/loopsdms/cq/"
|
||||
tal:attributes="src string:$movie?hostURL=$hostURL/"
|
||||
tal:attributes="src string:$movie?hostURL=$hostURL/&logoURL=$logoURL"
|
||||
quality=high
|
||||
width="960" height="680" name="scorm" align=""
|
||||
wmode="opaque"
|
||||
|
|
BIN
browser/flash/loops_logo.jpg
Normal file
BIN
browser/flash/loops_logo.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
|
@ -431,6 +431,7 @@
|
|||
<include package=".knowledge" />
|
||||
<include package=".organize" />
|
||||
<include package=".process" />
|
||||
<include package=".versioning" />
|
||||
<include package=".search" />
|
||||
<include package=".browser" />
|
||||
<include package=".xmlrpc" />
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
68
versioning/README.txt
Normal file
68
versioning/README.txt
Normal file
|
@ -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'
|
4
versioning/__init__.py
Normal file
4
versioning/__init__.py
Normal file
|
@ -0,0 +1,4 @@
|
|||
"""
|
||||
$Id$
|
||||
"""
|
||||
|
19
versioning/configure.zcml
Normal file
19
versioning/configure.zcml
Normal file
|
@ -0,0 +1,19 @@
|
|||
<!-- $Id$ -->
|
||||
|
||||
<configure
|
||||
xmlns:zope="http://namespaces.zope.org/zope"
|
||||
xmlns:browser="http://namespaces.zope.org/browser"
|
||||
i18n_domain="zope"
|
||||
>
|
||||
|
||||
<zope:adapter factory="loops.versioning.versionable.VersionableResource"
|
||||
trusted="True" />
|
||||
|
||||
<zope:class class="loops.versioning.versionable.VersionableResource">
|
||||
<require permission="zope.View"
|
||||
interface="loops.versioning.interfaces.IVersionable" />
|
||||
<require permission="zope.ManageContent"
|
||||
set_schema="loops.versioning.interfaces.IVersionable" />
|
||||
</zope:class>
|
||||
|
||||
</configure>
|
70
versioning/interfaces.py
Normal file
70
versioning/interfaces.py
Normal file
|
@ -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.
|
||||
"""
|
23
versioning/tests.py
Executable file
23
versioning/tests.py
Executable file
|
@ -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')
|
98
versioning/testsetup.py
Normal file
98
versioning/testsetup.py
Normal file
|
@ -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
|
||||
|
||||
|
149
versioning/versionable.py
Normal file
149
versioning/versionable.py
Normal file
|
@ -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
|
||||
|
44
versioning/versioninfo.py
Normal file
44
versioning/versioninfo.py
Normal file
|
@ -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()
|
14
view.py
14
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)
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue