250 lines
9.1 KiB
Python
250 lines
9.1 KiB
Python
#
|
|
# Copyright (c) 2014 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.
|
|
"""
|
|
|
|
from BTrees.OOBTree import OOBTree
|
|
from zope import component
|
|
from zope.component import adapts
|
|
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.meta.interfaces import IOptions
|
|
from cybertools.stateful.interfaces import IStateful
|
|
from cybertools.text.mimetypes import extensions
|
|
from cybertools.typology.interfaces import IType
|
|
from loops.common import adapted, baseObject
|
|
from loops.interfaces import IResource, IExternalFile
|
|
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
|
|
self.__parent__ = 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 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()
|
|
versionId = '.'.join('1' for x in self.versionLevels)
|
|
#versions['1.1'] = self.context
|
|
versions[versionId] = self.context
|
|
setattr(self.context, attrName, versions)
|
|
|
|
@Lazy
|
|
def versionLevels(self):
|
|
options = IOptions(self.master.getLoopsRoot()).useVersioning
|
|
if options:
|
|
if isinstance(options, list):
|
|
return options
|
|
return ['major', 'minor']
|
|
return []
|
|
|
|
@Lazy
|
|
def versionNumbers(self):
|
|
#return self.getVersioningAttribute('versionNumbers', (1, 1))
|
|
return self.getVersioningAttribute('versionNumbers',
|
|
tuple(1 for x in self.versionLevels))
|
|
|
|
@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)
|
|
|
|
@Lazy
|
|
def parent(self):
|
|
return self.getVersioningAttribute('parent', None)
|
|
|
|
@Lazy
|
|
def comment(self):
|
|
return self.getVersioningAttribute('comment', u'')
|
|
|
|
@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', None)
|
|
|
|
def getNotVersioned(self):
|
|
m = self.versionableMaster
|
|
return self.versionableMaster.getVersioningAttribute('notVersioned', False)
|
|
def setNotVersioned(self, value):
|
|
self.versionableMaster.setVersioningAttribute('notVersioned', bool(value))
|
|
notVersioned = property(getNotVersioned, setNotVersioned)
|
|
|
|
def createVersionObject(self, versionNumbers, variantIds, comment=u''):
|
|
versionableMaster = self.versionableMaster
|
|
versionableMaster.initVersions()
|
|
context = self.context
|
|
# create object
|
|
cls = context.__class__
|
|
obj = cls()
|
|
# set versioning attributes of new object
|
|
versionableObj = IVersionable(obj)
|
|
versionableObj.setVersioningAttribute('versionNumbers', tuple(versionNumbers))
|
|
versionableObj.setVersioningAttribute('variantIds', variantIds)
|
|
versionableObj.setVersioningAttribute('master', self.master)
|
|
versionableObj.setVersioningAttribute('parent', context)
|
|
versionableObj.setVersioningAttribute('comment', comment)
|
|
# generate name for new object, register in parent
|
|
versionId = versionableObj.versionId
|
|
name = self.generateName(getName(self.master),
|
|
extensions.get(context.contentType, ''),
|
|
versionId)
|
|
getParent(context)[name] = obj
|
|
# record version on master
|
|
self.versions[versionableObj.versionId] = obj
|
|
# set resource attributes
|
|
ti = IType(context).typeInterface
|
|
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
|
|
if 'context' in attrs:
|
|
attrs.remove('context')
|
|
if IExternalFile.providedBy(adaptedObj):
|
|
for name in ('data', 'externalAddress',):
|
|
attrs.remove(name)
|
|
for attr in attrs:
|
|
value = getattr(adaptedContext, attr)
|
|
if not callable(value):
|
|
setattr(adaptedObj, attr, value)
|
|
if IExternalFile.providedBy(adaptedObj):
|
|
adaptedObj.storageParams = adaptedContext.storageParams
|
|
adaptedObj.storageName = adaptedContext.storageName
|
|
extAddr = adaptedContext.externalAddress
|
|
newExtAddr = self.generateName(adapted(self.master).externalAddress,
|
|
extensions.get(context.contentType, ''),
|
|
versionId)
|
|
adaptedObj.externalAddress = newExtAddr
|
|
adaptedObj.data = adaptedContext.data
|
|
adaptedObj.externalAddress = newExtAddr # trigger post-processing
|
|
self.setStates(obj)
|
|
return obj
|
|
|
|
def createVersion(self, level=1, comment=u''):
|
|
# get the new version numbers
|
|
vn = list(IVersionable(self.currentVersion).versionNumbers)
|
|
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
|
|
obj = self.createVersionObject(vn, self.variantIds, comment)
|
|
# set attributes of the master version
|
|
self.versionableMaster.setVersioningAttribute('currentVersion', obj)
|
|
return obj
|
|
|
|
def generateName(self, name, ext, versionId):
|
|
if ext:
|
|
if ext and name.endswith(ext):
|
|
name = name[:-len(ext)]
|
|
else:
|
|
parts = name.rsplit('.', 1)
|
|
if len(parts) == 2 and len(parts[1]) <= 4:
|
|
name, ext = parts
|
|
#elif len(name) > 3 and name[-4] == '.':
|
|
# ext = name[-4:]
|
|
# name = name[:-4]
|
|
if not ext.startswith('.'):
|
|
ext = '.' + ext
|
|
return name + '_' + versionId + ext
|
|
|
|
def setStates(self, obj):
|
|
options = IOptions(self.master.getLoopsRoot()).organize.stateful.resource
|
|
if options:
|
|
for name in options:
|
|
stf = component.getAdapter(self.context, IStateful, name=name)
|
|
stfn = component.getAdapter(obj, IStateful, name=name)
|
|
stfn.state = stf.state
|
|
|
|
|
|
def cleanupVersions(context, event):
|
|
""" Called upon deletion of a resource or concept.
|
|
"""
|
|
vContext = IVersionable(context, None)
|
|
if vContext is None:
|
|
return
|
|
#rm = context.getLoopsRoot().getResourceManager()
|
|
rm = getParent(context)
|
|
if context == vContext.master:
|
|
toBeDeleted = []
|
|
for v in vContext.versions.values():
|
|
if v != context:
|
|
toBeDeleted.append(getName(v))
|
|
for name in toBeDeleted:
|
|
# TODO: notify(ObjectRemovedEvent(rm[name]))
|
|
del rm[name]
|
|
else:
|
|
vId = vContext.versionId
|
|
vMaster = vContext.versionableMaster
|
|
if vId in vMaster.versions:
|
|
del vMaster.versions[vId]
|
|
if vMaster.getVersioningAttribute('currentVersion', None) == context:
|
|
newCurrent = sorted(vMaster.versions.items())[-1][1]
|
|
vMaster.setVersioningAttribute('currentVersion', newCurrent)
|
|
|