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:
helmutm 2007-03-19 15:49:56 +00:00
parent 53810c1a19
commit c35d7fab4e
14 changed files with 492 additions and 7 deletions

View file

@ -5,6 +5,8 @@
xmlns="http://namespaces.zope.org/browser" xmlns="http://namespaces.zope.org/browser"
i18n_domain="zope"> i18n_domain="zope">
<resource name="loops_logo.jpg" file="loops_logo.jpg" />
<!-- Flash user interface --> <!-- Flash user interface -->
<page <page

View file

@ -1,6 +1,8 @@
<html tal:define="dummy view/setHeaders; <html tal:define="dummy view/setHeaders;
hostURL view/loopsUrl; hostURL view/loopsUrl;
movie context/++resource++loops.swf"> logoURL context/++resource++loops_logo.jpg;
movie context/++resource++loops.swf;
">
<head> <head>
<title>loops</title> <title>loops</title>
@ -24,7 +26,7 @@
<param name=quality value=high> <param name=quality value=high>
<param name="wmode" value="opaque"> <param name="wmode" value="opaque">
<embed src="loops.swf?hostURL=http://z3.loops.cy55.de/loopsdms/cq/" <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 quality=high
width="960" height="680" name="scorm" align="" width="960" height="680" name="scorm" align=""
wmode="opaque" wmode="opaque"

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -431,6 +431,7 @@
<include package=".knowledge" /> <include package=".knowledge" />
<include package=".organize" /> <include package=".organize" />
<include package=".process" /> <include package=".process" />
<include package=".versioning" />
<include package=".search" /> <include package=".search" />
<include package=".browser" /> <include package=".browser" />
<include package=".xmlrpc" /> <include package=".xmlrpc" />

View file

@ -147,6 +147,7 @@ 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
rels = getRelations(second=self, relationships=relationships) rels = getRelations(second=self, relationships=relationships)
return [r.first for r in rels] return [r.first for r in rels]

68
versioning/README.txt Normal file
View 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
View file

@ -0,0 +1,4 @@
"""
$Id$
"""

19
versioning/configure.zcml Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View file

@ -40,11 +40,12 @@ from cybertools.relation import DyadicRelation
from cybertools.relation.registry import getRelations from cybertools.relation.registry import getRelations
from cybertools.relation.interfaces import IRelationRegistry, IRelatable from cybertools.relation.interfaces import IRelationRegistry, IRelatable
from interfaces import IView, INode from loops.interfaces import IView, INode
from interfaces import IViewManager, INodeContained from loops.interfaces import IViewManager, INodeContained
from interfaces import ILoopsContained from loops.interfaces import ILoopsContained
from interfaces import ITargetRelation from loops.interfaces import ITargetRelation
from interfaces import IConcept from loops.interfaces import IConcept
from loops.versioning.versioninfo import getVersionInfo
class View(object): class View(object):
@ -202,6 +203,8 @@ 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
@ -211,6 +214,7 @@ class NodeTraverser(ItemTraverser):
else: else:
# 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)