Back to index

plone3  3.1.7
Repository.py
Go to the documentation of this file.
00001 ##############################################################################
00002 #
00003 # Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
00004 #
00005 # This software is subject to the provisions of the Zope Public License,
00006 # Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
00007 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
00008 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
00009 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
00010 # FOR A PARTICULAR PURPOSE
00011 #
00012 ##############################################################################
00013 
00014 __version__='$Revision: 1.14 $'[11:-2]
00015 
00016 import time
00017 from random import randint
00018 
00019 from Acquisition import Implicit, aq_parent, aq_inner
00020 from ZopeVersionHistory import ZopeVersionHistory
00021 from Globals import InitializeClass, Persistent
00022 from AccessControl import ClassSecurityInfo
00023 from Utility import use_vc_permission, _findPath
00024 from Utility import isAVersionableResource, VersionControlError, VersionInfo
00025 from DateTime.DateTime import DateTime
00026 from BTrees.OOBTree import OOBTree
00027 from BTrees.OIBTree import OIBTree
00028 
00029 from EventLog import LogEntry
00030 import Utility
00031 from nonversioned import getNonVersionedData, restoreNonVersionedData
00032 
00033 
00034 class Repository(Implicit, Persistent):
00035     """The repository implementation manages the actual data of versions
00036        and version histories. It does not handle user interface issues."""
00037 
00038     def __init__(self):
00039         # These keep track of symbolic label and branch names that
00040         # have been used to ensure that they don't collide.
00041         self._branches = OIBTree()
00042         self._branches['mainline'] = 1
00043         self._labels = OIBTree()
00044 
00045         self._histories = OOBTree()
00046         self._created = time.time()
00047 
00048     security = ClassSecurityInfo()
00049 
00050     security.declarePrivate('createVersionHistory')
00051     def createVersionHistory(self, object):
00052         """Internal: create a new version history for a resource."""
00053         # When one creates the first version in a version history, neither
00054         # the version or version history yet have a _p_jar, which causes
00055         # copy operations to fail. To work around that, we share our _p_jar.
00056         history_id = None
00057         while history_id is None or self._histories.has_key(history_id):
00058             history_id = str(randint(1, 9999999999))
00059         history = ZopeVersionHistory(history_id, object)
00060         self._histories[history_id] = history
00061         return history.__of__(self)
00062 
00063     security.declarePrivate('getVersionHistory')
00064     def getVersionHistory(self, history_id):
00065         """Internal: return a version history given a version history id."""
00066         return self._histories[history_id].__of__(self)
00067 
00068     security.declarePrivate('replaceState')
00069     def replaceState(self, obj, new_state):
00070         """Internal: replace the state of a persistent object.
00071         """
00072         non_versioned = getNonVersionedData(obj)
00073         # XXX There ought to be some way to do this more cleanly.
00074         # This fills the __dict__ of the old object with new state.
00075         # The other way to achieve the desired effect is to replace
00076         # the object in its container, but this method preserves the
00077         # identity of the object.
00078         if obj.__class__ is not new_state.__class__:
00079             raise VersionControlError(
00080                 "The class of the versioned object has changed. %s != %s"
00081                 % (repr(obj.__class__, new_state.__class__)))
00082         obj._p_changed = 1
00083         for key in obj.__dict__.keys():
00084             if not new_state.__dict__.has_key(key):
00085                 del obj.__dict__[key]
00086         for key, value in new_state.__dict__.items():
00087             obj.__dict__[key] = value
00088         if non_versioned:
00089             # Restore the non-versioned data into the new state.
00090             restoreNonVersionedData(obj, non_versioned)
00091         return obj
00092 
00093     #####################################################################
00094     # This is the implementation of the public version control interface.
00095     #####################################################################
00096 
00097     security.declarePublic('isAVersionableResource')
00098     def isAVersionableResource(self, obj):
00099         # For now, an object must be persistent (have its own db record)
00100         # in order to be considered a versionable resource.
00101         return isAVersionableResource(obj)
00102 
00103     security.declarePublic('isUnderVersionControl')
00104     def isUnderVersionControl(self, object):
00105         return hasattr(object, '__vc_info__')
00106 
00107     security.declarePublic('isResourceUpToDate')
00108     def isResourceUpToDate(self, object, require_branch=0):
00109         info = self.getVersionInfo(object)
00110         history = self.getVersionHistory(info.history_id)
00111         branch = 'mainline'
00112         if info.sticky:
00113             if info.sticky[0] == 'B':
00114                 branch = info.sticky[1]
00115             elif require_branch:
00116                 # The object is updated to a particular version
00117                 # rather than a branch.  The caller
00118                 # requires a branch.
00119                 return 0
00120         return history.isLatestVersion(info.version_id, branch)
00121 
00122     security.declarePublic('isResourceChanged')
00123     def isResourceChanged(self, object):
00124         # Return true if the state of a resource has changed in a transaction
00125         # *after* the version bookkeeping was saved. Note that this method is
00126         # not appropriate for detecting changes within a transaction!
00127         info = self.getVersionInfo(object)
00128         itime = getattr(info, '_p_mtime', None)
00129         if itime is None:
00130             return 0
00131         mtime = Utility._findModificationTime(object)
00132         if mtime is None:
00133             return 0
00134         return mtime > itime
00135 
00136     security.declarePublic('getVersionInfo')
00137     def getVersionInfo(self, object):
00138         info = getattr(object, '__vc_info__', None)
00139         if info is not None:
00140             return info
00141         raise VersionControlError(
00142             'The specified resource is not under version control.'
00143             )
00144 
00145     security.declareProtected(use_vc_permission, 'applyVersionControl')
00146     def applyVersionControl(self, object, message=None):
00147         if self.isUnderVersionControl(object):
00148             raise VersionControlError(
00149                 'The resource is already under version control.'
00150                 )
00151         if not self.isAVersionableResource(object):
00152             raise VersionControlError(
00153                 'This resource cannot be put under version control.'
00154                 )
00155 
00156         # Need to check the parent to see if the container of the object
00157         # being put under version control is itself a version-controlled
00158         # object. If so, we need to use the branch id of the container.
00159         branch = 'mainline'
00160         parent = aq_parent(aq_inner(object))
00161         p_info = getattr(parent, '__vc_info__', None)
00162         if p_info is not None:
00163             sticky = p_info.sticky
00164             if sticky and sticky[0] == 'B':
00165                 branch = sticky[1]
00166 
00167         # Create a new version history and initial version object.
00168         history = self.createVersionHistory(object)
00169         version = history.createVersion(object, branch)
00170 
00171         history_id = history.getId()
00172         version_id = version.getId()
00173 
00174         # Add bookkeeping information to the version controlled object.
00175         info = VersionInfo(history_id, version_id, VersionInfo.CHECKED_IN)
00176         if branch != 'mainline':
00177             info.sticky = ('B', branch)
00178         object.__vc_info__ = info
00179 
00180         # Save an audit record of the action being performed.
00181         history.addLogEntry(version_id,
00182                             LogEntry.ACTION_CHECKIN,
00183                             _findPath(object),
00184                             message is None and 'Initial checkin.' or message
00185                             )
00186         return object
00187 
00188     security.declareProtected(use_vc_permission, 'checkoutResource')
00189     def checkoutResource(self, object):
00190         info = self.getVersionInfo(object)
00191         if info.status != info.CHECKED_IN:
00192             raise VersionControlError(
00193                 'The selected resource is already checked out.'
00194                 )
00195 
00196         if info.sticky and info.sticky[0] != 'B':
00197             raise VersionControlError(
00198                 'The selected resource has been updated to a particular '
00199                 'version, label or date. The resource must be updated to '
00200                 'the mainline or a branch before it may be checked out.'
00201                 )
00202 
00203         if not self.isResourceUpToDate(object):
00204             raise VersionControlError(
00205                 'The selected resource is not up to date!'
00206                 )
00207 
00208         history = self.getVersionHistory(info.history_id)
00209         ob_path = _findPath(object)
00210 
00211         # Save an audit record of the action being performed.
00212         history.addLogEntry(info.version_id,
00213                             LogEntry.ACTION_CHECKOUT,
00214                             ob_path
00215                             )
00216 
00217         # Update bookkeeping information.
00218         newinfo = info.clone()
00219         newinfo.status = newinfo.CHECKED_OUT
00220         object.__vc_info__ = newinfo
00221         return object
00222 
00223     security.declareProtected(use_vc_permission, 'checkinResource')
00224     def checkinResource(self, object, message=''):
00225         info = self.getVersionInfo(object)
00226         if info.status != info.CHECKED_OUT:
00227             raise VersionControlError(
00228                 'The selected resource is not checked out.'
00229                 )
00230 
00231         if info.sticky and info.sticky[0] != 'B':
00232             raise VersionControlError(
00233                 'The selected resource has been updated to a particular '
00234                 'version, label or date. The resource must be updated to '
00235                 'the mainline or a branch before it may be checked in.'
00236                 )
00237 
00238         if not self.isResourceUpToDate(object):
00239             raise VersionControlError(
00240                 'The selected resource is not up to date!'
00241                 )
00242 
00243         history = self.getVersionHistory(info.history_id)
00244         ob_path = _findPath(object)
00245 
00246         branch = 'mainline'
00247         if info.sticky is not None and info.sticky[0] == 'B':
00248             branch = info.sticky[1]
00249 
00250         version = history.createVersion(object, branch)
00251 
00252         # Save an audit record of the action being performed.
00253         history.addLogEntry(version.getId(),
00254                             LogEntry.ACTION_CHECKIN,
00255                             ob_path,
00256                             message
00257                             )
00258 
00259         # Update bookkeeping information.
00260         newinfo = info.clone()
00261         newinfo.version_id = version.getId()
00262         newinfo.status = newinfo.CHECKED_IN
00263         object.__vc_info__ = newinfo
00264         return object
00265 
00266     security.declareProtected(use_vc_permission, 'uncheckoutResource')
00267     def uncheckoutResource(self, object):
00268         info = self.getVersionInfo(object)
00269         if info.status != info.CHECKED_OUT:
00270             raise VersionControlError(
00271                 'The selected resource is not checked out.'
00272                 )
00273 
00274         history = self.getVersionHistory(info.history_id)
00275         ob_path = _findPath(object)
00276 
00277         version = history.getVersionById(info.version_id)
00278         new_obj = version.copyState()
00279 
00280         # Save an audit record of the action being performed.
00281         history.addLogEntry(info.version_id,
00282                             LogEntry.ACTION_UNCHECKOUT,
00283                             ob_path
00284                             )
00285 
00286         # Replace the state of the object with a reverted state.
00287         new_obj = self.replaceState(object, new_obj)
00288 
00289         # Update bookkeeping information.
00290         newinfo = info.clone()
00291         newinfo.version_id = version.getId()
00292         newinfo.status = newinfo.CHECKED_IN
00293         new_obj.__vc_info__ = newinfo
00294         return new_obj
00295 
00296     security.declareProtected(use_vc_permission, 'updateResource')
00297     def updateResource(self, object, selector=None):
00298         info = self.getVersionInfo(object)
00299         if info.status != info.CHECKED_IN:
00300             raise VersionControlError(
00301                 'The selected resource must be checked in to be updated.'
00302                 )
00303 
00304         history = self.getVersionHistory(info.history_id)
00305         version = None
00306         sticky = info.sticky
00307 
00308         if not selector:
00309             # If selector is null, update to the latest version taking any
00310             # sticky attrs into account (branch, date). Note that the sticky
00311             # tag could also be a date or version id. We don't bother checking
00312             # for those, since in both cases we do nothing (because we'll
00313             # always be up to date until the sticky tag changes).
00314             if sticky and sticky[0] == 'L':
00315                 # A label sticky tag, so update to that label (since it is
00316                 # possible, but unlikely, that the label has been moved).
00317                 version = history.getVersionByLabel(sticky[1])
00318             elif sticky and sticky[0] == 'B':
00319                 # A branch sticky tag. Update to latest version on branch.
00320                 version = history.getLatestVersion(selector)
00321             else:
00322                 # Update to mainline, forgetting any date or version id
00323                 # sticky tag that was previously associated with the object.
00324                 version = history.getLatestVersion('mainline')
00325                 sticky = None
00326         else:
00327             # If the selector is non-null, we find the version specified
00328             # and update the sticky tag. Later we'll check the version we
00329             # found and decide whether we really need to update the object.
00330             if history.hasVersionId(selector):
00331                 version = history.getVersionById(selector)
00332                 sticky = ('V', selector)
00333 
00334             elif self._labels.has_key(selector):
00335                 version = history.getVersionByLabel(selector)
00336                 sticky = ('L', selector)
00337 
00338             elif self._branches.has_key(selector):
00339                 version = history.getLatestVersion(selector)
00340                 if selector == 'mainline':
00341                     sticky = None
00342                 else:
00343                     sticky = ('B', selector)
00344             else:
00345                 try:    date = DateTime(selector)
00346                 except:
00347                     raise VersionControlError(
00348                         'Invalid version selector: %s' % selector
00349                         )
00350                 else:
00351                     timestamp = date.timeTime()
00352                     sticky = ('D', timestamp)
00353                     # Fix!
00354                     branch = history.findBranchId(info.version_id)
00355                     version = history.getVersionByDate(branch, timestamp)
00356 
00357         # If the state of the resource really needs to be changed, do the
00358         # update and make a log entry for the update.
00359         version_id = version and version.getId() or info.version_id
00360         new_object = object
00361         if version and (version_id != info.version_id):
00362             new_object = version.copyState()
00363             new_object = self.replaceState(object, new_object)
00364 
00365             history.addLogEntry(version_id,
00366                                 LogEntry.ACTION_UPDATE,
00367                                 _findPath(new_object)
00368                                 )
00369 
00370         # Update bookkeeping information.
00371         newinfo = info.clone(1)
00372         newinfo.version_id = version_id
00373         newinfo.status = newinfo.CHECKED_IN
00374         if sticky is not None:
00375             newinfo.sticky = sticky
00376         new_object.__vc_info__ = newinfo
00377         return new_object
00378 
00379     security.declareProtected(use_vc_permission, 'labelResource')
00380     def labelResource(self, object, label, force=0):
00381         info = self.getVersionInfo(object)
00382         if info.status != info.CHECKED_IN:
00383             raise VersionControlError(
00384                 'The selected resource must be checked in to be labeled.'
00385                 )
00386 
00387         # Make sure that labels and branch ids do not collide.
00388         if self._branches.has_key(label) or label == 'mainline':
00389             raise VersionControlError(
00390                 'The label value given is already in use as an activity id.'
00391                 )
00392         if not self._labels.has_key(label):
00393             self._labels[label] = 1
00394 
00395         history = self.getVersionHistory(info.history_id)
00396         history.labelVersion(info.version_id, label, force)
00397         return object
00398 
00399     security.declareProtected(use_vc_permission, 'makeActivity')
00400     def makeActivity(self, object, branch_id):
00401         # Note - this is not part of the official version control API yet.
00402         # It is here to allow unit testing of the architectural aspects
00403         # that are already in place to support activities in the future.
00404 
00405         info = self.getVersionInfo(object)
00406         if info.status != info.CHECKED_IN:
00407             raise VersionControlError(
00408                 'The selected resource must be checked in.'
00409                 )
00410 
00411         branch_id = branch_id or None
00412 
00413         # Make sure that activity ids and labels do not collide.
00414         if self._labels.has_key(branch_id) or branch_id == 'mainline':
00415             raise VersionControlError(
00416                 'The value given is already in use as a version label.'
00417                 )
00418 
00419         if not self._branches.has_key(branch_id):
00420             self._branches[branch_id] = 1
00421 
00422         history = self.getVersionHistory(info.history_id)
00423 
00424         if history._branches.has_key(branch_id):
00425             raise VersionControlError(
00426                 'The resource is already associated with the given activity.'
00427                 )
00428 
00429         history.createBranch(branch_id, info.version_id)
00430         return object
00431 
00432     security.declareProtected(use_vc_permission, 'getVersionOfResource')
00433     def getVersionOfResource(self, history_id, selector):
00434         history = self.getVersionHistory(history_id)
00435         sticky = None
00436 
00437         if not selector or selector == 'mainline':
00438             version = history.getLatestVersion('mainline')
00439         else:
00440             if history.hasVersionId(selector):
00441                 version = history.getVersionById(selector)
00442                 sticky = ('V', selector)
00443 
00444             elif self._labels.has_key(selector):
00445                 version = history.getVersionByLabel(selector)
00446                 sticky = ('L', selector)
00447 
00448             elif self._branches.has_key(selector):
00449                 version = history.getLatestVersion(selector)
00450                 sticky = ('B', selector)
00451             else:
00452                 try: date = DateTime(selector)
00453                 except:
00454                     raise VersionControlError(
00455                         'Invalid version selector: %s' % selector
00456                         )
00457                 else:
00458                     timestamp = date.timeTime()
00459                     sticky = ('D', timestamp)
00460                     version = history.getVersionByDate('mainline', timestamp)
00461 
00462         object = version.copyState()
00463 
00464         info = VersionInfo(history_id, version.getId(), VersionInfo.CHECKED_IN)
00465         if sticky is not None:
00466             info.sticky = sticky
00467         object.__vc_info__ = info
00468         return object
00469 
00470     security.declareProtected(use_vc_permission, 'getVersionIds')
00471     def getVersionIds(self, object):
00472         info = self.getVersionInfo(object)
00473         history = self.getVersionHistory(info.history_id)
00474         return history.getVersionIds()
00475 
00476     security.declareProtected(use_vc_permission, 'getLabelsForResource')
00477     def getLabelsForResource(self, object):
00478         info = self.getVersionInfo(object)
00479         history = self.getVersionHistory(info.history_id)
00480         return history.getLabels()
00481 
00482     security.declareProtected(use_vc_permission, 'getLogEntries')
00483     def getLogEntries(self, object):
00484         info = self.getVersionInfo(object)
00485         history = self.getVersionHistory(info.history_id)
00486         return history.getLogEntries()
00487 
00488 InitializeClass(Repository)