--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zodbbrowser/history.py Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,138 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+import inspect
+
+# import interfaces
+from ZODB.interfaces import IConnection
+from pyams_zodbbrowser.interfaces import IObjectHistory, IDatabaseHistory
+
+# import packages
+from persistent import Persistent
+from pyams_utils.adapter import adapter_config
+from pyams_zodbbrowser import cache
+from ZODB.utils import tid_repr
+from zope.interface import implementer
+
+
+@adapter_config(context=Persistent, provides=IObjectHistory)
+@implementer(IObjectHistory)
+class ZodbObjectHistory(object):
+
+ def __init__(self, obj):
+ self._obj = obj
+ self._connection = self._obj._p_jar
+ self._storage = self._connection._storage
+ self._oid = self._obj._p_oid
+ self._history = None
+ self._by_tid = {}
+
+ def __len__(self):
+ if self._history is None:
+ self._load()
+ return len(self._history)
+
+ def _load(self):
+ """Load history of changes made to a Persistent object.
+
+ Returns a list of dictionaries, from latest revision to the oldest.
+ The dicts have various interesting pieces of data, such as:
+
+ tid -- transaction ID (a byte string, usually 8 bytes)
+ time -- transaction timestamp (number of seconds since the Unix epoch)
+ user_name -- name of the user responsible for the change
+ description -- short description (often a URL)
+
+ See the 'history' method of ZODB.interfaces.IStorage.
+ """
+ size = 999999999999 # "all of it"; ought to be sufficient
+ # NB: ClientStorage violates the interface by calling the last
+ # argument 'length' instead of 'size'. To avoid problems we must
+ # use positional argument syntax here.
+ # NB: FileStorage in ZODB 3.8 has a mandatory second argument 'version'
+ # FileStorage in ZODB 3.9 doesn't accept a 'version' argument at all.
+ # This check is ugly, but I see no other options if I want to support
+ # both ZODB versions :(
+ if 'version' in inspect.getargspec(self._storage.history)[0]:
+ version = None
+ self._history = self._storage.history(self._oid, version, size)
+ else:
+ self._history = self._storage.history(self._oid, size=size)
+ self._index_by_tid()
+
+ def _index_by_tid(self):
+ for record in self._history:
+ self._by_tid[record['tid']] = record
+
+ def __getitem__(self, item):
+ if self._history is None:
+ self._load()
+ return self._history[item]
+
+ def lastChange(self, tid=None):
+ if self._history is None:
+ self._load()
+ if tid in self._by_tid:
+ # optimization
+ return tid
+ # sadly ZODB has no API for get revision at or before tid, so
+ # we have to find the exact tid
+ for record in self._history:
+ # we assume records are ordered by tid, newest to oldest
+ if tid is None or record['tid'] <= tid:
+ return record['tid']
+ raise KeyError('%r did not exist in or before transaction %r' %
+ (self._obj, tid_repr(tid)))
+
+ def loadStatePickle(self, tid=None):
+ return self._connection._storage.loadSerial(self._obj._p_oid,
+ self.lastChange(tid))
+
+ def loadState(self, tid=None):
+ return self._connection.oldstate(self._obj, self.lastChange(tid))
+
+ def rollback(self, tid):
+ state = self.loadState(tid)
+ if state != self.loadState():
+ self._obj.__setstate__(state)
+ self._obj._p_changed = True
+
+
+@adapter_config(context=IConnection, provides=IDatabaseHistory)
+@implementer(IObjectHistory)
+class ZodbHistory(object):
+
+ def __init__(self, connection):
+ self._connection = connection
+ self._storage = connection._storage
+ self._tids = cache.getStorageTids(self._storage)
+
+ @property
+ def tids(self):
+ return tuple(self._tids) # readonlify
+
+ def __len__(self):
+ return len(self._tids)
+
+ def __iter__(self):
+ return self._storage.iterator()
+
+ def __getitem__(self, index):
+ if isinstance(index, slice):
+ tids = self._tids[index]
+ if not tids:
+ return []
+ return self._storage.iterator(tids[0], tids[-1])