Back to index

plone3  3.1.7
athistoryaware.py
Go to the documentation of this file.
00001 ################################################################################
00002 #
00003 # Copyright (c) 2002-2005, Benjamin Saller <bcsaller@ideasuite.com>, and
00004 #                              the respective authors. All rights reserved.
00005 # For a list of Archetypes contributors see docs/CREDITS.txt.
00006 #
00007 # Redistribution and use in source and binary forms, with or without
00008 # modification, are permitted provided that the following conditions are met:
00009 #
00010 # * Redistributions of source code must retain the above copyright notice, this
00011 #   list of conditions and the following disclaimer.
00012 # * Redistributions in binary form must reproduce the above copyright notice,
00013 #   this list of conditions and the following disclaimer in the documentation
00014 #   and/or other materials provided with the distribution.
00015 # * Neither the name of the author nor the names of its contributors may be used
00016 #   to endorse or promote products derived from this software without specific
00017 #   prior written permission.
00018 #
00019 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
00020 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
00021 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
00022 # FOR A PARTICULAR PURPOSE.
00023 #
00024 ################################################################################
00025 """Archetypes history awareness"""
00026 __author__  = 'Martijn Pieters <mj@zopatista.com>'
00027 
00028 import itertools
00029 
00030 from DateTime import DateTime
00031 from OFS.History import HystoryJar
00032 from Acquisition import aq_parent
00033 from BTrees.OOBTree import OOBTree
00034 from Globals import InitializeClass
00035 
00036 from AccessControl import ClassSecurityInfo
00037 
00038 from annotations import AT_ANN_KEYS
00039 from interfaces.athistoryaware import IATHistoryAware
00040 
00041 # A note about this implementation
00042 #
00043 # Archetypes now stores field data in a __annotations__ BTree, and many (if not
00044 # all) of these field data objects are persistent themselves. This means that
00045 # each of these objects will get it's own revisions in the ZODB, and retrieving
00046 # an object's historic revision will not retrieve historic revisions of 
00047 # subobjects.
00048 #
00049 # The following implementation will merge the histories for the main object,
00050 # the __annotations__ BTree, and any persistent annotition that uses an 
00051 # Archetypes key. It does not recurse into those objects though (which would
00052 # make the implementation far more complex), so fields like File and Image will
00053 # not be correctly reconstructed.
00054 #
00055 # When an edit is made to an archetype, it may be that only one field is 
00056 # altered and only that field is then recorded in a transaction. Adding or
00057 # removing a field from the annotations will result in a new revision of the
00058 # __annotations__ BTree, but the main object remains unaffected. Editing a 
00059 # title will affect self, and no fields stored in annotations are altered. 
00060 #
00061 # The following table illustrates a series of such transactions 
00062 # (__annotations__ is shortened to 'ann', crosses mark comitted revisions,
00063 # a slash marks a removed object):
00064 #
00065 #  tid | self | ann | fld1 | fld2 | fld3 |
00066 # ---------------------------------------------------------------------
00067 #   1  |  x   |  x  |  x   |  x   |      |  Object created
00068 #   2  |  x   |     |  x   |      |      |  Field 1 and title edited
00069 #   3  |      |  x  |      |      |  x   | Field 3 added
00070 #   4  |      |  x  |      |  x   |  /   |  Field 2 edited, field 3 removed
00071 #   5  |  x   |     |  x   |      |      |  More edits
00072 #
00073 # Now, to construct the last 3 historic revisions, one has to pull together
00074 # various object revisions. For tid 5, to construct the full object, one has to
00075 # take tid 4 for __annotations__ and field 2, and no revision for field 3. 
00076 # Tid 3 combines tid 2 for self and field 1, tid 1 for field 2 with tid 3 
00077 # versions of __annotations__ and field 3.
00078 #
00079 # Note that packing does not remove older revisions still referenced through
00080 # backpointers from not-packed revisions. So, if a pack takes place removing
00081 # items from before tid 4, the revisions for self and field 1 at tid 2, and
00082 # field 2 at tid 1 are still retained. Field 3 will be completely purged, as
00083 # are the revisions for __annotations__ from tids 1 and 3.
00084 
00085 # The OFS.History.historicalRevision method fails for OOBTrees.
00086 def _historicalRevision(self, tid):
00087     state = self._p_jar.oldstate(self, tid)
00088     try:
00089         rev = self.__class__.__basicnew__()
00090     except AttributeError:
00091         rev = self.__class__()
00092     rev._p_jar = HystoryJar(self._p_jar)
00093     rev._p_oid = self._p_oid
00094     rev._p_serial = tid
00095     rev.__setstate__(state)
00096     rev._p_changed = 0
00097     return rev
00098 
00099 def _objectRevisions(obj, limit=10):
00100     """Iterate over (thread id, persistent object revisions), up to limit"""
00101     for rev in obj._p_jar.db().history(obj._p_oid, None, limit):
00102         tid = rev.get('tid', None) or rev.get('serial', None)
00103         if not tid: # Apparently not all storages provide this?
00104             return
00105         # Set 'tid' so we don't have to test for 'serial' again
00106         rev['tid'] = tid
00107         rev['object'] = _historicalRevision(obj, tid)
00108         yield tid, rev
00109 
00110 class ATHistoryAwareMixin:
00111     """Archetypes history aware mixin class
00112     
00113     Provide ZODB revisions, constructed from older transactions. Note that 
00114     these transactions are available only up to the last pack.
00115 
00116     """
00117 
00118     __implements__ = (IATHistoryAware,)
00119 
00120     security       = ClassSecurityInfo()
00121 
00122 
00123     security.declarePrivate('_constructAnnotatedHistory')
00124     def _constructAnnotatedHistory(self, max=10):
00125         """Reconstruct historical revisions of archetypes objects
00126         
00127         Merges revisions to self with revisions to archetypes-related items
00128         in __annotations__. Yields at most max recent revisions.
00129         
00130         """
00131         # All relevant historical states by transaction id
00132         # For every tid, keep a dict with object revisions, keyed on annotation
00133         # id, or None for self and '__annotations__' for the ann OOBTree
00134         # Initialize with self revisions
00135         history = dict([(tid, {None: rev})
00136                         for (tid, rev) in _objectRevisions(self, max)])
00137             
00138         if not getattr(self, '__annotations__', None):
00139             # No annotations, just return the history we have for self
00140             # Note that if this object had __annotations__ in a past 
00141             # transaction they will be ignored! Working around this is a
00142             # YAGNI I think though.
00143             for tid in sorted(history.keys()):
00144                 yield history[tid][None]
00145             return
00146             
00147         # Now find all __annotation__ revisions, and the annotation keys
00148         # used in those.
00149         annotation_key_objects = {}
00150         isatkey = lambda k, aak=AT_ANN_KEYS: filter(k.startswith, aak)
00151         # Loop over max revisions of the __annotations__ object to retrieve
00152         # all keys (and more importantly, their objects so we can get revisions)
00153         for tid, rev in _objectRevisions(self.__annotations__, max):
00154             history.setdefault(tid, {})['__annotations__'] = rev
00155             revision = rev['object']
00156             for key in itertools.ifilter(isatkey, revision.iterkeys()):
00157                 if not hasattr(revision[key], '_p_jar'):
00158                     continue # Not persistent
00159                 if key not in annotation_key_objects:
00160                     annotation_key_objects[key] = revision[key]
00161                     
00162         # For all annotation keys, get their revisions
00163         for key, obj in annotation_key_objects.iteritems():
00164             for tid, rev in _objectRevisions(obj, max):
00165                 history.setdefault(tid, {})[key] = rev
00166         del annotation_key_objects
00167                 
00168         # Now we merge the annotation and object revisions into one for each
00169         # transaction id, and yield the results
00170         tids = sorted(history.iterkeys(), reverse=True)
00171         def find_revision(tids, key):
00172             """First revision of key in a series of tids"""
00173             has_revision = lambda t, h=history, k=key: k in h[t]
00174             next_tid = itertools.ifilter(has_revision, tids).next()
00175             return history[next_tid][key]
00176         
00177         for i, tid in enumerate(tids[:max]):
00178             revision = find_revision(tids[i:], None)
00179             obj = revision['object']
00180             # Track size to maintain correct metadata
00181             size = revision['size']
00182             
00183             anns_rev = find_revision(tids[i:], '__annotations__')
00184             size += anns_rev['size']
00185             anns = anns_rev['object']
00186             
00187             # We use a temporary OOBTree to avoid _p_jar complaints from the
00188             # transaction machinery
00189             tempbtree = OOBTree()
00190             tempbtree.__setstate__(anns.__getstate__())
00191             
00192             # Find annotation revisions and insert
00193             for key in itertools.ifilter(isatkey, tempbtree.iterkeys()):
00194                 if not hasattr(tempbtree[key], '_p_jar'):
00195                     continue # Not persistent
00196                 value_rev = find_revision(tids[i:], key)
00197                 size += value_rev['size']
00198                 tempbtree[key] = value_rev['object']
00199                 
00200             # Now transfer the tembtree state over to anns, effectively 
00201             # bypassing the transaction registry while maintaining BTree 
00202             # integrity
00203             anns.__setstate__(tempbtree.__getstate__())
00204             anns._p_changed = 0
00205             del tempbtree
00206             
00207             # Do a similar hack to set anns on the main object
00208             state = obj.__getstate__()
00209             state['__annotations__'] = anns
00210             obj.__setstate__(state)
00211             obj._p_changed = 0
00212             
00213             # Update revision metadata if needed
00214             if revision['tid'] != tid:
00215                 # any other revision will do; only size and object are unique
00216                 revision = history[tid].values()[0].copy()
00217                 revision['object'] = obj
00218                 
00219             # Correct size based on merged records
00220             revision['size'] = size
00221             
00222             # clean up as we go
00223             del history[tid]
00224             
00225             yield revision
00226 
00227     security.declarePrivate('getHistories')
00228     def getHistories(self, max=10):
00229         """Iterate over historic revisions.
00230         
00231         Yields (object, time, transaction_note, user) tuples, where object
00232         is an object revision approximating what was committed at that time,
00233         with the current acquisition context.
00234 
00235         Object revisions include correct archetype-related annotation revisions 
00236         (in __annotations__); other persistent sub-objects are in their current 
00237         revision, not historical!
00238         
00239         """
00240         
00241         parent = aq_parent(self)
00242         for revision in self._constructAnnotatedHistory(max):
00243             obj = revision['object'].__of__(parent)
00244             yield (obj, DateTime(revision['time']), revision['description'], 
00245                    revision['user_name'])
00246 
00247 InitializeClass(ATHistoryAwareMixin)