--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,19 @@
+
+syntax: regexp
+^develop-eggs$
+syntax: regexp
+^parts$
+syntax: regexp
+^bin$
+syntax: regexp
+^\.installed\.cfg$
+syntax: regexp
+^\.settings$
+syntax: regexp
+^build$
+syntax: regexp
+^dist$
+syntax: regexp
+^\.idea$
+syntax: regexp
+.*\.pyc$
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/.installed.cfg Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,71 @@
+[buildout]
+installed_develop_eggs =
+parts = package i18n pyflakes test
+
+[package]
+__buildout_installed__ = /home/tflorac/Dropbox/src/PyAMS/pyams_zodbbrowser/bin/pserve
+ /home/tflorac/Dropbox/src/PyAMS/pyams_zodbbrowser/bin/ptweens
+ /home/tflorac/Dropbox/src/PyAMS/pyams_zodbbrowser/bin/pshell
+ /home/tflorac/Dropbox/src/PyAMS/pyams_zodbbrowser/bin/prequest
+ /home/tflorac/Dropbox/src/PyAMS/pyams_zodbbrowser/bin/pviews
+ /home/tflorac/Dropbox/src/PyAMS/pyams_zodbbrowser/bin/proutes
+ /home/tflorac/Dropbox/src/PyAMS/pyams_zodbbrowser/bin/pcreate
+ /home/tflorac/Dropbox/src/PyAMS/pyams_zodbbrowser/bin/pdistreport
+__buildout_signature__ = zc.recipe.egg-2.0.1-py3.4.egg setuptools-12.3-py3.4.egg zc.buildout-2.3.1-py3.4.egg
+_b = /home/tflorac/Dropbox/src/PyAMS/pyams_zodbbrowser/bin
+_d = /home/tflorac/Dropbox/src/PyAMS/pyams_zodbbrowser/develop-eggs
+_e = /var/local/env/pyams/eggs
+bin-directory = /home/tflorac/Dropbox/src/PyAMS/pyams_zodbbrowser/bin
+develop-eggs-directory = /home/tflorac/Dropbox/src/PyAMS/pyams_zodbbrowser/develop-eggs
+eggs = pyams_zodbbrowser
+ pyramid
+ zope.component
+ zope.interface
+eggs-directory = /var/local/env/pyams/eggs
+recipe = zc.recipe.egg
+
+[i18n]
+__buildout_installed__ = /home/tflorac/Dropbox/src/PyAMS/pyams_zodbbrowser/bin/pybabel
+ /home/tflorac/Dropbox/src/PyAMS/pyams_zodbbrowser/bin/polint
+ /home/tflorac/Dropbox/src/PyAMS/pyams_zodbbrowser/bin/pot-create
+__buildout_signature__ = zc.recipe.egg-2.0.1-py3.4.egg setuptools-12.3-py3.4.egg zc.buildout-2.3.1-py3.4.egg
+_b = /home/tflorac/Dropbox/src/PyAMS/pyams_zodbbrowser/bin
+_d = /home/tflorac/Dropbox/src/PyAMS/pyams_zodbbrowser/develop-eggs
+_e = /var/local/env/pyams/eggs
+bin-directory = /home/tflorac/Dropbox/src/PyAMS/pyams_zodbbrowser/bin
+develop-eggs-directory = /home/tflorac/Dropbox/src/PyAMS/pyams_zodbbrowser/develop-eggs
+eggs = babel
+ lingua
+eggs-directory = /var/local/env/pyams/eggs
+recipe = zc.recipe.egg
+
+[pyflakes]
+__buildout_installed__ = /home/tflorac/Dropbox/src/PyAMS/pyams_zodbbrowser/bin/pyflakes
+ /home/tflorac/Dropbox/src/PyAMS/pyams_zodbbrowser/bin/pyflakes
+__buildout_signature__ = zc.recipe.egg-2.0.1-py3.4.egg setuptools-12.3-py3.4.egg zc.buildout-2.3.1-py3.4.egg
+_b = /home/tflorac/Dropbox/src/PyAMS/pyams_zodbbrowser/bin
+_d = /home/tflorac/Dropbox/src/PyAMS/pyams_zodbbrowser/develop-eggs
+_e = /var/local/env/pyams/eggs
+bin-directory = /home/tflorac/Dropbox/src/PyAMS/pyams_zodbbrowser/bin
+develop-eggs-directory = /home/tflorac/Dropbox/src/PyAMS/pyams_zodbbrowser/develop-eggs
+eggs = pyflakes
+eggs-directory = /var/local/env/pyams/eggs
+entry-points = pyflakes=pyflakes.scripts.pyflakes:main
+initialization = if not sys.argv[1:]: sys.argv[1:] = ["src"]
+recipe = zc.recipe.egg
+scripts = pyflakes
+
+[test]
+__buildout_installed__ = /home/tflorac/Dropbox/src/PyAMS/pyams_zodbbrowser/parts/test
+ /home/tflorac/Dropbox/src/PyAMS/pyams_zodbbrowser/bin/test
+__buildout_signature__ = zc.recipe.testrunner-2.0.0-py3.4.egg zc.recipe.egg-2.0.1-py3.4.egg setuptools-12.3-py3.4.egg zope.testrunner-4.4.6-py3.4.egg zc.buildout-2.3.1-py3.4.egg zope.interface-4.1.2-py3.4-linux-x86_64.egg zope.exceptions-4.0.7-py3.4.egg six-1482e89f68d85eea27f4ed7749df2819
+_b = /home/tflorac/Dropbox/src/PyAMS/pyams_zodbbrowser/bin
+_d = /home/tflorac/Dropbox/src/PyAMS/pyams_zodbbrowser/develop-eggs
+_e = /var/local/env/pyams/eggs
+bin-directory = /home/tflorac/Dropbox/src/PyAMS/pyams_zodbbrowser/bin
+develop-eggs-directory = /home/tflorac/Dropbox/src/PyAMS/pyams_zodbbrowser/develop-eggs
+eggs = pyams_zodbbrowser [test]
+eggs-directory = /var/local/env/pyams/eggs
+location = /home/tflorac/Dropbox/src/PyAMS/pyams_zodbbrowser/parts/test
+recipe = zc.recipe.testrunner
+script = /home/tflorac/Dropbox/src/PyAMS/pyams_zodbbrowser/bin/test
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/LICENSE Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,42 @@
+Zope Public License (ZPL) Version 2.1
+=====================================
+
+A copyright notice accompanies this license document that identifies
+the copyright holders.
+
+This license has been certified as open source. It has also been designated
+as GPL compatible by the Free Software Foundation (FSF).
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+ 1. Redistributions in source code must retain the accompanying copyright
+ notice, this list of conditions, and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the accompanying copyright
+ notice, this list of conditions, and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Names of the copyright holders must not be used to endorse or promote
+ products derived from this software without prior written permission
+ from the copyright holders.
+ 4. The right to distribute this software or to use it for any purpose does
+ not give you the right to use Servicemarks (sm) or Trademarks (tm) of the
+ copyright holders. Use of them is covered by separate agreement with the
+ copyright holders.
+ 5. If any files are modified, you must cause the modified files to carry
+ prominent notices stating that you changed the files and the date of any
+ change.
+
+
+Disclaimer
+==========
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESSED
+OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/MANIFEST.in Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,5 @@
+include *.txt
+recursive-include docs *
+recursive-include src *
+global-exclude *.pyc
+global-exclude *.*~
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/bootstrap.py Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,178 @@
+##############################################################################
+#
+# Copyright (c) 2006 Zope Foundation and Contributors.
+# 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.
+#
+##############################################################################
+"""Bootstrap a buildout-based project
+
+Simply run this script in a directory containing a buildout.cfg.
+The script accepts buildout command-line options, so you can
+use the -c option to specify an alternate configuration file.
+"""
+
+import os
+import shutil
+import sys
+import tempfile
+
+from optparse import OptionParser
+
+tmpeggs = tempfile.mkdtemp()
+
+usage = '''\
+[DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options]
+
+Bootstraps a buildout-based project.
+
+Simply run this script in a directory containing a buildout.cfg, using the
+Python that you want bin/buildout to use.
+
+Note that by using --find-links to point to local resources, you can keep
+this script from going over the network.
+'''
+
+parser = OptionParser(usage=usage)
+parser.add_option("-v", "--version", help="use a specific zc.buildout version")
+
+parser.add_option("-t", "--accept-buildout-test-releases",
+ dest='accept_buildout_test_releases',
+ action="store_true", default=False,
+ help=("Normally, if you do not specify a --version, the "
+ "bootstrap script and buildout gets the newest "
+ "*final* versions of zc.buildout and its recipes and "
+ "extensions for you. If you use this flag, "
+ "bootstrap and buildout will get the newest releases "
+ "even if they are alphas or betas."))
+parser.add_option("-c", "--config-file",
+ help=("Specify the path to the buildout configuration "
+ "file to be used."))
+parser.add_option("-f", "--find-links",
+ help=("Specify a URL to search for buildout releases"))
+parser.add_option("--allow-site-packages",
+ action="store_true", default=False,
+ help=("Let bootstrap.py use existing site packages"))
+
+
+options, args = parser.parse_args()
+
+######################################################################
+# load/install setuptools
+
+try:
+ if options.allow_site_packages:
+ import setuptools
+ import pkg_resources
+ from urllib.request import urlopen
+except ImportError:
+ from urllib2 import urlopen
+
+ez = {}
+exec(urlopen('https://bootstrap.pypa.io/ez_setup.py').read(), ez)
+
+if not options.allow_site_packages:
+ # ez_setup imports site, which adds site packages
+ # this will remove them from the path to ensure that incompatible versions
+ # of setuptools are not in the path
+ import site
+ # inside a virtualenv, there is no 'getsitepackages'.
+ # We can't remove these reliably
+ if hasattr(site, 'getsitepackages'):
+ for sitepackage_path in site.getsitepackages():
+ sys.path[:] = [x for x in sys.path if sitepackage_path not in x]
+
+setup_args = dict(to_dir=tmpeggs, download_delay=0)
+ez['use_setuptools'](**setup_args)
+import setuptools
+import pkg_resources
+
+# This does not (always?) update the default working set. We will
+# do it.
+for path in sys.path:
+ if path not in pkg_resources.working_set.entries:
+ pkg_resources.working_set.add_entry(path)
+
+######################################################################
+# Install buildout
+
+ws = pkg_resources.working_set
+
+cmd = [sys.executable, '-c',
+ 'from setuptools.command.easy_install import main; main()',
+ '-mZqNxd', tmpeggs]
+
+find_links = os.environ.get(
+ 'bootstrap-testing-find-links',
+ options.find_links or
+ ('http://downloads.buildout.org/'
+ if options.accept_buildout_test_releases else None)
+ )
+if find_links:
+ cmd.extend(['-f', find_links])
+
+setuptools_path = ws.find(
+ pkg_resources.Requirement.parse('setuptools')).location
+
+requirement = 'zc.buildout'
+version = options.version
+if version is None and not options.accept_buildout_test_releases:
+ # Figure out the most recent final version of zc.buildout.
+ import setuptools.package_index
+ _final_parts = '*final-', '*final'
+
+ def _final_version(parsed_version):
+ for part in parsed_version:
+ if (part[:1] == '*') and (part not in _final_parts):
+ return False
+ return True
+ index = setuptools.package_index.PackageIndex(
+ search_path=[setuptools_path])
+ if find_links:
+ index.add_find_links((find_links,))
+ req = pkg_resources.Requirement.parse(requirement)
+ if index.obtain(req) is not None:
+ best = []
+ bestv = None
+ for dist in index[req.project_name]:
+ distv = dist.parsed_version
+ if _final_version(distv):
+ if bestv is None or distv > bestv:
+ best = [dist]
+ bestv = distv
+ elif distv == bestv:
+ best.append(dist)
+ if best:
+ best.sort()
+ version = best[-1].version
+if version:
+ requirement = '=='.join((requirement, version))
+cmd.append(requirement)
+
+import subprocess
+if subprocess.call(cmd, env=dict(os.environ, PYTHONPATH=setuptools_path)) != 0:
+ raise Exception(
+ "Failed to execute command:\n%s" % repr(cmd)[1:-1])
+
+######################################################################
+# Import and run buildout
+
+ws.add_entry(tmpeggs)
+ws.require(requirement)
+import zc.buildout.buildout
+
+if not [a for a in args if '=' not in a]:
+ args.append('bootstrap')
+
+# if -c was provided, we push it back into args for buildout' main function
+if options.config_file is not None:
+ args[0:0] = ['-c', options.config_file]
+
+zc.buildout.buildout.main(args)
+shutil.rmtree(tmpeggs)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/buildout.cfg Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,68 @@
+[buildout]
+eggs-directory = /var/local/env/pyams/eggs
+
+socket-timeout = 3
+show-picked-versions = true
+newest = false
+
+allow-hosts =
+ bitbucket.org
+ *.python.org
+ *.sourceforge.net
+ github.com
+
+#extends = http://download.ztfy.org/webapp/ztfy.webapp.dev.cfg
+versions = versions
+newest = false
+#allow-picked-versions = false
+
+src = src
+develop =
+ .
+ ../pyams_form
+ ../pyams_pagelet
+ ../pyams_skin
+ ../pyams_template
+ ../pyams_utils
+ ../pyams_viewlet
+ ../pyams_zmi
+ ../ext/lingua
+
+parts =
+ package
+ i18n
+ pyflakes
+ test
+
+[package]
+recipe = zc.recipe.egg
+eggs =
+ pyams_zodbbrowser
+ pyramid
+ zope.component
+ zope.interface
+
+[i18n]
+recipe = zc.recipe.egg
+eggs =
+ babel
+ lingua
+
+[pyflakes]
+recipe = zc.recipe.egg
+eggs = pyflakes
+scripts = pyflakes
+entry-points = pyflakes=pyflakes.scripts.pyflakes:main
+initialization = if not sys.argv[1:]: sys.argv[1:] = ["${buildout:src}"]
+
+[pyflakesrun]
+recipe = collective.recipe.cmd
+on_install = true
+cmds = ${buildout:develop}/bin/${pyflakes:scripts}
+
+[test]
+recipe = zc.recipe.testrunner
+eggs = pyams_zodbbrowser [test]
+
+[versions]
+pyams_base = 0.1.0
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/setup.py Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,68 @@
+#
+# 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.
+#
+
+"""
+This module contains pyams_zodbbrowser package
+"""
+import os
+from setuptools import setup, find_packages
+
+DOCS = os.path.join(os.path.dirname(__file__),
+ 'docs')
+
+README = os.path.join(DOCS, 'README.txt')
+HISTORY = os.path.join(DOCS, 'HISTORY.txt')
+
+version = '0.1.0'
+long_description = open(README).read() + '\n\n' + open(HISTORY).read()
+
+tests_require = []
+
+setup(name='pyams_zodbbrowser',
+ version=version,
+ description="PyAMS ZODB browser integration",
+ long_description=long_description,
+ classifiers=[
+ "License :: OSI Approved :: Zope Public License",
+ "Development Status :: 4 - Beta",
+ "Programming Language :: Python",
+ "Framework :: Pyramid",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ ],
+ keywords='Pyramid PyAMS ZODB browser',
+ author='Thierry Florac',
+ author_email='tflorac@ulthar.net',
+ url='http://hg.ztfy.org/pyams/pyams_zodbbrowser',
+ license='ZPL',
+ packages=find_packages('src'),
+ package_dir={'': 'src'},
+ namespace_packages=[],
+ include_package_data=True,
+ package_data={'': ['*.zcml', '*.txt', '*.pt', '*.pot', '*.po', '*.mo', '*.png', '*.gif', '*.jpeg', '*.jpg', '*.css', '*.js']},
+ zip_safe=False,
+ # uncomment this to be able to run tests with setup.py
+ test_suite="pyams_zodbbrowser.tests.test_utilsdocs.test_suite",
+ tests_require=tests_require,
+ extras_require=dict(test=tests_require),
+ install_requires=[
+ 'setuptools',
+ # -*- Extra requirements: -*-
+ 'pyams_zmi',
+ 'pyramid',
+ 'zope.component',
+ 'zope.interface',
+ ],
+ entry_points={
+ 'fanstatic.libraries': [
+ 'pyams_zodbbrowser = pyams_zodbbrowser:library'
+ ]
+ })
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zodbbrowser.egg-info/PKG-INFO Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,18 @@
+Metadata-Version: 1.1
+Name: pyams-zodbbrowser
+Version: 0.1.0
+Summary: PyAMS ZODB browser integration
+Home-page: http://hg.ztfy.org/pyams/pyams_zodbbrowser
+Author: Thierry Florac
+Author-email: tflorac@ulthar.net
+License: ZPL
+Description:
+
+
+Keywords: Pyramid PyAMS ZODB browser
+Platform: UNKNOWN
+Classifier: License :: OSI Approved :: Zope Public License
+Classifier: Development Status :: 4 - Beta
+Classifier: Programming Language :: Python
+Classifier: Framework :: Pyramid
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zodbbrowser.egg-info/SOURCES.txt Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,46 @@
+MANIFEST.in
+setup.py
+docs/HISTORY.txt
+docs/README.txt
+src/pyams_zodbbrowser/__init__.py
+src/pyams_zodbbrowser/btreesupport.py
+src/pyams_zodbbrowser/cache.py
+src/pyams_zodbbrowser/configure.zcml
+src/pyams_zodbbrowser/diff.py
+src/pyams_zodbbrowser/history.py
+src/pyams_zodbbrowser/state.py
+src/pyams_zodbbrowser/testing.py
+src/pyams_zodbbrowser/value.py
+src/pyams_zodbbrowser.egg-info/PKG-INFO
+src/pyams_zodbbrowser.egg-info/SOURCES.txt
+src/pyams_zodbbrowser.egg-info/dependency_links.txt
+src/pyams_zodbbrowser.egg-info/entry_points.txt
+src/pyams_zodbbrowser.egg-info/namespace_packages.txt
+src/pyams_zodbbrowser.egg-info/not-zip-safe
+src/pyams_zodbbrowser.egg-info/requires.txt
+src/pyams_zodbbrowser.egg-info/top_level.txt
+src/pyams_zodbbrowser/doctests/README.txt
+src/pyams_zodbbrowser/interfaces/__init__.py
+src/pyams_zodbbrowser/locales/pyams_zodbbrowser.pot
+src/pyams_zodbbrowser/locales/fr/LC_MESSAGES/pyams_zodbbrowser.mo
+src/pyams_zodbbrowser/locales/fr/LC_MESSAGES/pyams_zodbbrowser.po
+src/pyams_zodbbrowser/tests/__init__.py
+src/pyams_zodbbrowser/tests/realdb.py
+src/pyams_zodbbrowser/tests/test_btreesupport.py
+src/pyams_zodbbrowser/tests/test_cache.py
+src/pyams_zodbbrowser/tests/test_diff.py
+src/pyams_zodbbrowser/tests/test_history.py
+src/pyams_zodbbrowser/tests/test_state.py
+src/pyams_zodbbrowser/tests/test_utilsdocs.py
+src/pyams_zodbbrowser/tests/test_utilsdocstrings.py
+src/pyams_zodbbrowser/tests/test_value.py
+src/pyams_zodbbrowser/zmi/__init__.py
+src/pyams_zodbbrowser/zmi/views.py
+src/pyams_zodbbrowser/zmi/resources/css/zodbbrowser.css
+src/pyams_zodbbrowser/zmi/resources/css/zodbbrowser.min.css
+src/pyams_zodbbrowser/zmi/resources/img/collapse.png
+src/pyams_zodbbrowser/zmi/resources/img/expand.png
+src/pyams_zodbbrowser/zmi/resources/js/zodbbrowser.js
+src/pyams_zodbbrowser/zmi/resources/js/zodbbrowser.min.js
+src/pyams_zodbbrowser/zmi/templates/zodbhistory.pt
+src/pyams_zodbbrowser/zmi/templates/zodbinfo.pt
\ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zodbbrowser.egg-info/dependency_links.txt Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,1 @@
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zodbbrowser.egg-info/entry_points.txt Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,3 @@
+[fanstatic.libraries]
+pyams_zodbbrowser = pyams_zodbbrowser:library
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zodbbrowser.egg-info/namespace_packages.txt Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,1 @@
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zodbbrowser.egg-info/not-zip-safe Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,1 @@
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zodbbrowser.egg-info/requires.txt Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,7 @@
+setuptools
+pyams_zmi
+pyramid
+zope.component
+zope.interface
+
+[test]
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zodbbrowser.egg-info/top_level.txt Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,1 @@
+pyams_zodbbrowser
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zodbbrowser/__init__.py Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,35 @@
+#
+# 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'
+
+
+from fanstatic import Library
+
+from pyramid.i18n import TranslationStringFactory
+_ = TranslationStringFactory('pyams_zodbbrowser')
+
+
+library = Library('pyams_zodbbrowser', 'zmi/resources')
+
+
+def includeme(config):
+ """Pyramid include"""
+
+ # add translations
+ config.add_translation_dirs('pyams_zodbbrowser:locales')
+
+ # load registry components
+ config.scan()
+
+ if hasattr(config, 'load_zcml'):
+ config.load_zcml('configure.zcml')
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zodbbrowser/btreesupport.py Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,238 @@
+#
+# 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.
+#
+"""
+BTrees are commonly used in the Zope world. This modules exposes the
+contents of BTrees nicely, abstracting away the implementation details.
+
+In the DB, every BTree can be represented by more than one persistent object,
+every one of those versioned separately. This is part of what makes BTrees
+efficient.
+
+The format of the picked BTree state is nicely documented in ZODB's source
+code, specifically, BTreeTemplate.c and BucketTemplate.c.
+"""
+from pyams_utils.request import check_request
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+
+# import interfaces
+from pyams_zodbbrowser.interfaces import IStateInterpreter, IObjectHistory
+
+# import packages
+
+# be compatible with Zope 3.4, but prefer the modern package structure
+from pyams_utils.adapter import adapter_config
+from pyams_zodbbrowser.history import ZodbObjectHistory
+from pyams_zodbbrowser.state import GenericState
+from BTrees.OOBTree import OOBTree, OOBucket
+from zope.container.folder import Folder
+from zope.container.btree import BTreeContainer
+from zope.interface import implementer
+
+
+@adapter_config(context=OOBTree, provides=IObjectHistory)
+@implementer(IObjectHistory)
+class OOBTreeHistory(ZodbObjectHistory):
+
+ def _load(self):
+ # find all objects (tree and buckets) that have ever participated in
+ # this OOBTree
+ queue = [self._obj]
+ seen = set(self._oid)
+ history_of = {}
+ while queue:
+ obj = queue.pop(0)
+ history = history_of[obj._p_oid] = ZodbObjectHistory(obj)
+ for d in history:
+ state = history.loadState(d['tid'])
+ if state and len(state) > 1:
+ bucket = state[1]
+ if bucket._p_oid not in seen:
+ queue.append(bucket)
+ seen.add(bucket._p_oid)
+ # merge the histories of all objects
+ by_tid = {}
+ for h in list(history_of.values()):
+ for d in h:
+ by_tid.setdefault(d['tid'], d)
+ self._history = list(by_tid.values())
+ self._history.sort(key=lambda d: d['tid'], reverse=True)
+ self._index_by_tid()
+
+ def _lastRealChange(self, tid=None):
+ return ZodbObjectHistory(self._obj).lastChange(tid)
+
+ def loadStatePickle(self, tid=None):
+ # lastChange would return the tid that modified self._obj or any
+ # of its subobjects, thanks to the history merging done by _load.
+ # We need the real last change value.
+ # XXX: this is used to show the pickled size of an object. It
+ # will be misleading for BTrees if we show just the size for the
+ # main BTree object while we're hiding all the individual buckets.
+ return self._connection._storage.loadSerial(self._obj._p_oid,
+ self._lastRealChange(tid))
+
+ def loadState(self, tid=None):
+ # lastChange would return the tid that modified self._obj or any
+ # of its subobjects, thanks to the history merging done by _load.
+ # We need the real last change value.
+ return self._connection.oldstate(self._obj, self._lastRealChange(tid))
+
+ def rollback(self, tid):
+ state = self.loadState(tid)
+ if state != self.loadState():
+ self._obj.__setstate__(state)
+ self._obj._p_changed = True
+
+ while state and len(state) > 1:
+ bucket = state[1]
+ bucket_history = IObjectHistory(bucket)
+ state = bucket_history.loadState(tid)
+ if state != bucket_history.loadState():
+ bucket.__setstate__(state)
+ bucket._p_changed = True
+
+
+@adapter_config(context=(OOBTree, tuple, None), provides=IStateInterpreter)
+@implementer(IStateInterpreter)
+class OOBTreeState(object):
+ """Non-empty OOBTrees have a complicated tuple structure."""
+
+ def __init__(self, type, state, tid):
+ self.btree = OOBTree()
+ self.btree.__setstate__(state)
+ self.state = state
+ # Large btrees have more than one bucket; we have to load old states
+ # to all of them. See BTreeTemplate.c and BucketTemplate.c for
+ # docs of the pickled state format.
+ while state and len(state) > 1:
+ bucket = state[1]
+ state = IObjectHistory(bucket).loadState(tid)
+ # XXX this is dangerous!
+ bucket.__setstate__(state)
+
+ self._items = list(self.btree.items())
+ self._dict = dict(self.btree)
+
+ # now UNDO to avoid dangerous side effects,
+ # see https://bugs.launchpad.net/zodbbrowser/+bug/487243
+ state = self.state
+ while state and len(state) > 1:
+ bucket = state[1]
+ state = IObjectHistory(bucket).loadState()
+ bucket.__setstate__(state)
+
+ def getError(self):
+ return None
+
+ def getName(self):
+ return None
+
+ def getParent(self):
+ return None
+
+ def listAttributes(self):
+ return None
+
+ def listItems(self):
+ return self._items
+
+ def asDict(self):
+ return self._dict
+
+
+@adapter_config(context=(OOBTree, type(None), None), provides=IStateInterpreter)
+class EmptyOOBTreeState(OOBTreeState):
+ """Empty OOBTrees pickle to None."""
+
+
+@adapter_config(context=(Folder, dict, None), provides=IStateInterpreter)
+class FolderState(GenericState):
+ """Convenient access to a Folder's items"""
+
+ def listItems(self):
+ data = self.state.get('data')
+ if not data:
+ return []
+ # data will be an OOBTree
+ loadedstate = IObjectHistory(data).loadState(self.tid)
+ registry = check_request().registry
+ return registry.getMultiAdapter((data, loadedstate, self.tid),
+ IStateInterpreter).listItems()
+
+
+@adapter_config(context=(BTreeContainer, dict, None), provides=IStateInterpreter)
+class BTreeContainerState(GenericState):
+ """Convenient access to a BTreeContainer's items"""
+
+ def listItems(self):
+ # This is not a typo; BTreeContainer really uses
+ # _SampleContainer__data, for BBB
+ data = self.state.get('_SampleContainer__data')
+ if not data:
+ return []
+ # data will be an OOBTree
+ loadedstate = IObjectHistory(data).loadState(self.tid)
+ registry = check_request().registry
+ return registry.getMultiAdapter((data, loadedstate, self.tid),
+ IStateInterpreter).listItems()
+
+
+@adapter_config(context=(OOBucket, tuple, None), provides=IStateInterpreter)
+class OOBucketState(GenericState):
+ """A single OOBTree bucket, should you wish to look at the internals
+
+ Here's the state description direct from BTrees/BucketTemplate.c::
+
+ * For a set bucket (self->values is NULL), a one-tuple or two-tuple. The
+ * first element is a tuple of keys, of length self->len. The second element
+ * is the next bucket, present if and only if next is non-NULL:
+ *
+ * (
+ * (keys[0], keys[1], ..., keys[len-1]),
+ * <self->next iff non-NULL>
+ * )
+ *
+ * For a mapping bucket (self->values is not NULL), a one-tuple or two-tuple.
+ * The first element is a tuple interleaving keys and values, of length
+ * 2 * self->len. The second element is the next bucket, present iff next is
+ * non-NULL:
+ *
+ * (
+ * (keys[0], values[0], keys[1], values[1], ...,
+ * keys[len-1], values[len-1]),
+ * <self->next iff non-NULL>
+ * )
+
+ OOBucket is a mapping bucket; OOSet is a set bucket.
+ """
+
+ def getError(self):
+ return None
+
+ def getName(self):
+ return None
+
+ def getParent(self):
+ return None
+
+ def listAttributes(self):
+ return [('_next', self.state[1] if len(self.state) > 1 else None)]
+
+ def listItems(self):
+ return list(zip(self.state[0][::2], self.state[0][1::2]))
+
+ def asDict(self):
+ return dict(self.listAttributes(), _items=dict(self.listItems()))
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zodbbrowser/cache.py Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,59 @@
+#
+# 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 time
+import weakref
+
+# import interfaces
+
+# import packages
+
+
+MINUTES = 60
+HOURS = 60 * MINUTES
+
+STORAGE_TIDS = weakref.WeakKeyDictionary()
+
+
+def expired(cache_dict, cache_for):
+ if 'last_update' not in cache_dict:
+ return True
+ return time.time() > cache_dict['last_update'] + cache_for
+
+
+def getStorageTids(storage, cache_for=5 * MINUTES):
+ cache_dict = STORAGE_TIDS.setdefault(storage, {})
+ if expired(cache_dict, cache_for):
+ if cache_dict.get('tids'):
+ first = cache_dict['tids'][-1]
+ last = cache_dict['tids'][-1]
+ try:
+ first_record = next(storage.iterator())
+ except StopIteration:
+ first_record = None
+ if first_record and first_record.tid == first:
+ # okay, look for new transactions appended at the end
+ new = [t.tid for t in storage.iterator(start=last)]
+ if new and new[0] == last:
+ del new[0]
+ cache_dict['tids'].extend(new)
+ else:
+ # first record changed, we must've packed the DB
+ cache_dict['tids'] = [t.tid for t in storage.iterator()]
+ else:
+ cache_dict['tids'] = [t.tid for t in storage.iterator()]
+ cache_dict['last_update'] = time.time()
+ return cache_dict['tids']
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zodbbrowser/configure.zcml Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,6 @@
+<configure
+ xmlns="http://pylonshq.com/pyramid">
+
+ <include package="pyramid_zcml" />
+
+</configure>
\ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zodbbrowser/diff.py Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,136 @@
+#
+# 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
+from html import escape
+
+# import interfaces
+from pyams_zodbbrowser.interfaces import IValueRenderer
+
+# import packages
+
+
+ADDED = 'added'
+REMOVED = 'removed'
+CHANGED = 'changed to'
+
+
+def compareDicts(new, old):
+ """Compare two state dictionaries, return dict."""
+ diffs = {}
+ for key, value in list(new.items()):
+ if key not in old:
+ diffs[key] = (ADDED, value, None)
+ elif old[key] != value:
+ diffs[key] = (CHANGED, value, old[key])
+ for key, value in list(old.items()):
+ if key not in new:
+ diffs[key] = (REMOVED, None, value)
+ return diffs
+
+
+def isascii(s):
+ """See if the string can be safely converted to unicode."""
+ try:
+ s.encode('ascii')
+ except UnicodeError:
+ return False
+ else:
+ return True
+
+
+def compareTuples(new, old):
+ """Compare two tuples.
+
+ Return (common_prefix, middle_of_old, middle_of_new, common_suffix)
+ """
+ first = 0
+ for oldval, newval in zip(old, new):
+ if oldval != newval:
+ break
+ first += 1
+ last = 0
+ for oldval, newval in zip(reversed(old[first:]), reversed(new[first:])):
+ if oldval != newval:
+ break
+ last += 1
+ return (old[:first],
+ old[first:len(old) - last],
+ new[first:len(new) - last],
+ old[len(old) - last:])
+
+
+def compareTuplesHTML(new, old, tid=None, indent=''):
+ """Compare two tuples, return HTML."""
+ html = [indent + '<div class="diff">\n']
+ prefix, removed, added, suffix = compareTuples(new, old)
+ if len(prefix) > 0:
+ html.append(indent + ' <div class="diffitem %s">\n' % 'same')
+ if len(prefix) == 1:
+ html.append(indent + ' first item kept the same\n')
+ else:
+ html.append(indent + ' first %d items kept the same\n' % len(prefix))
+ html.append(indent + ' </div>\n')
+ for oldval in removed:
+ html.append(indent + ' <div class="diffitem %s">\n' % REMOVED)
+ html.append(indent + ' %s %s\n' % (
+ REMOVED, IValueRenderer(oldval).render(tid)))
+ html.append(indent + ' </div>\n')
+ for newval in added:
+ html.append(indent + ' <div class="diffitem %s">\n' % ADDED)
+ html.append(indent + ' %s %s\n' % (
+ ADDED, IValueRenderer(newval).render(tid)))
+ html.append(indent + ' </div>\n')
+ if len(suffix) > 0:
+ html.append(indent + ' <div class="diffitem %s">\n' % 'same')
+ if len(suffix) == 1:
+ html.append(indent + ' last item kept the same\n')
+ else:
+ html.append(indent + ' last %d items kept the same\n' % len(suffix))
+ html.append(indent + ' </div>\n')
+ html.append(indent + '</div>\n')
+ return ''.join(html)
+
+
+def compareDictsHTML(new, old, tid=None, indent=''):
+ """Compare two state dictionaries, return HTML."""
+ html = [indent + '<div class="diff">\n']
+ diff = compareDicts(new, old)
+ for key, (action, newvalue, oldvalue) in sorted(list(diff.items()),
+ key=lambda k_v: (str(type(k_v[0])), k_v[0])):
+ what = action.split()[0]
+ html.append(indent + ' <div class="diffitem %s">\n' % escape(what))
+ if isinstance(key, str) and isascii(key):
+ html.append(indent + ' <strong>%s</strong>: ' % escape(key))
+ else:
+ html.append(indent + ' <strong>%s</strong>: ' % IValueRenderer(key).render(tid))
+ if (action == CHANGED and isinstance(oldvalue, dict) and isinstance(newvalue, dict)):
+ html.append('dictionary changed:\n')
+ html.append(compareDictsHTML(newvalue, oldvalue, tid, indent=indent + ' '))
+ elif (action == CHANGED and isinstance(oldvalue, tuple) and isinstance(newvalue, tuple)):
+ html.append('tuple changed:\n')
+ html.append(compareTuplesHTML(newvalue, oldvalue, tid, indent=indent + ' '))
+ else:
+ html.append(action)
+ html.append(' ')
+ if action == REMOVED:
+ value = oldvalue
+ else:
+ value = newvalue
+ html.append(IValueRenderer(value).render(tid))
+ html.append('\n')
+ html.append(indent + ' </div>\n')
+ html.append(indent + '</div>\n')
+ return ''.join(html)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zodbbrowser/doctests/README.txt Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,3 @@
+=========================
+pyams_zodbbrowser package
+=========================
--- /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])
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zodbbrowser/interfaces/__init__.py Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,145 @@
+#
+# 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 interfaces
+
+# import packages
+from zope.interface import Interface
+
+
+class IObjectHistory(Interface):
+ """History of persistent object state.
+
+ Adapt a persistent object to IObjectHistory.
+ """
+
+ def __len__():
+ """Return the number of history records."""
+
+ def __getitem__(n):
+ """Return n-th history record.
+
+ Records are ordered by age, from latest (index 0) to oldest.
+
+ Each record is a dictonary with at least the following items:
+
+ tid -- transaction ID (a byte string, usually 8 bytes)
+ time -- transaction timestamp (Unix time_t value)
+ user_name -- name of the user responsible for the change
+ description -- short description (often a URL)
+
+ """
+
+ def lastChange(tid=None):
+ """Return the last transaction at or before tid.
+
+ If tid is not specified, returns the very last transaction that
+ modified this object.
+
+ Will raise KeyError if object did not exist before the given
+ transaction.
+ """
+
+ def loadState(tid=None):
+ """Load and return the object's historical state at or before tid.
+
+ Returns the unpicked state, not an actual persistent object.
+ """
+
+ def loadStatePickle(tid=None):
+ """Load and return the object's historical state at or before tid.
+
+ Returns the picked state as a string.
+ """
+
+ def rollback(tid):
+ """Roll back object state to what it was at or before tid."""
+
+
+class IDatabaseHistory(Interface):
+ """History of the entire database.
+
+ Adapt a connection object to IObjectHistory.
+ """
+
+ def __iter__(n):
+ """Return an iterator over the history record.
+
+ Records are ordered by age, from oldest (index 0) to newest.
+
+ Each record provides ZODB.interfaces.an IStorageTransactionInformation.
+ """
+
+
+class IValueRenderer(Interface):
+ """Renderer of attribute values."""
+
+ def render(tid=None, can_link=True):
+ """Render object value to HTML.
+
+ Hyperlinks to other persistent objects will be limited to versions
+ at or older than the specified transaction id (``tid``).
+ """
+
+
+class IStateInterpreter(Interface):
+ """Interprets persistent object state.
+
+ Usually you adapt a tuple (object, state, tid) to IStateInterpreter to
+ figure out how a certain object type represents its state for pickling.
+ The tid may be None or may be a transaction id, and is supplied in case
+ you need to look at states of other objects to make a full sense of this
+ one.
+ """
+
+ def getError():
+ """Return an error message, if there was an error loading this state."""
+
+ def listAttributes():
+ """Return the attributes of this object as tuples (name, value).
+
+ The order of the attributes returned is irrelevant.
+
+ May return None to indicate that this kind of object cannot
+ store attributes.
+ """
+
+ def listItems():
+ """Return the items of this object as tuples (name, value).
+
+ The order of the attributes returned matters.
+
+ Often these are not stored directly, but extracted from an attribute
+ and presented as items for convenience.
+
+ May return None to indicate that this kind of object is not a
+ container and cannot store items.
+ """
+
+ def getParent():
+ """Return the parent of this object."""
+
+ def getName():
+ """Return the name of this object."""
+
+ def asDict():
+ """Return the state expressed as an attribute dictionary.
+
+ The state should combine the attributes and items somehow, to present
+ a complete picture for the purpose of comparing these dictionaries
+ while looking for changes.
+ """
Binary file src/pyams_zodbbrowser/locales/fr/LC_MESSAGES/pyams_zodbbrowser.mo has changed
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zodbbrowser/locales/fr/LC_MESSAGES/pyams_zodbbrowser.po Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,21 @@
+#
+# French translations for PACKAGE package
+# This file is distributed under the same license as the PACKAGE package.
+# Thierry Florac <tflorac@ulthar.net>, 2015.
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE 1.0\n"
+"POT-Creation-Date: 2015-03-02 10:09+0100\n"
+"PO-Revision-Date: 2015-03-02 10:12+0100\n"
+"Last-Translator: Thierry Florac <tflorac@ulthar.net>\n"
+"Language-Team: French <traduc@traduc.org>\n"
+"Language: fr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Lingua 3.10.dev0\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: src/pyams_zodbbrowser/zmi/views.py:127
+msgid "ZODB browser"
+msgstr "Navigateur ZODB"
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zodbbrowser/locales/pyams_zodbbrowser.pot Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,21 @@
+#
+# SOME DESCRIPTIVE TITLE
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2015.
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE 1.0\n"
+"POT-Creation-Date: 2015-03-02 10:09+0100\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Lingua 3.10.dev0\n"
+
+#: ./src/pyams_zodbbrowser/zmi/views.py:127
+msgid "ZODB browser"
+msgstr ""
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zodbbrowser/state.py Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,329 @@
+#
+# 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 logging
+
+# import interfaces
+from zope.interface.interfaces import IInterface
+from zope.traversing.interfaces import IContainmentRoot
+
+# import packages
+from persistent.dict import PersistentDict
+from persistent.list import PersistentList
+from persistent.mapping import PersistentMapping
+from pyams_utils.adapter import adapter_config
+from pyams_utils.request import check_request
+from pyams_zodbbrowser.interfaces import IStateInterpreter, IObjectHistory
+from ZODB.utils import u64
+from zope.interface import implementer, Interface
+from zope.interface.interface import InterfaceClass
+
+import zope.interface.declarations
+
+# be compatible with Zope 3.4, but prefer the modern package structure
+from zope.container.sample import SampleContainer
+from zope.container.ordered import OrderedContainer
+from zope.container.contained import ContainedProxy
+
+
+log = logging.getLogger(__name__)
+
+
+real_Provides = zope.interface.declarations.Provides
+
+
+def install_provides_hack():
+ """Monkey-patch zope.interface.Provides with a more lenient version.
+
+ A common result of missing modules in sys.path is that you cannot
+ unpickle objects that have been marked with directlyProvides() to
+ implement interfaces that aren't currently available. Those interfaces
+ are replaced by persistent broken placeholders, which aren classes,
+ not interfaces, and aren't iterable, causing TypeErrors during unpickling.
+ """
+ zope.interface.declarations.Provides = Provides
+
+
+def flatten_interfaces(args):
+ result = []
+ for a in args:
+ if isinstance(a, (list, tuple)):
+ result.extend(flatten_interfaces(a))
+ elif IInterface.providedBy(a):
+ result.append(a)
+ else:
+ log.warning(' replacing %s with a placeholder', repr(a))
+ result.append(InterfaceClass(a.__name__,
+ __module__='broken ' + a.__module__))
+ return result
+
+
+def Provides(cls, *interfaces):
+ try:
+ return real_Provides(cls, *interfaces)
+ except TypeError as e:
+ log.warning('Suppressing TypeError while unpickling Provides: %s', e)
+ args = flatten_interfaces(interfaces)
+ return real_Provides(cls, *args)
+
+
+@implementer(IStateInterpreter)
+class ZodbObjectState(object):
+
+ def __init__(self, obj, tid=None, _history=None):
+ self.obj = obj
+ if _history is None:
+ _history = IObjectHistory(self.obj)
+ else:
+ assert _history._obj is self.obj
+ self.history = _history
+ self.tid = None
+ self.requestedTid = tid
+ self.loadError = None
+ self.pickledState = ''
+ self._load()
+
+ def _load(self):
+ self.tid = self.history.lastChange(self.requestedTid)
+ try:
+ self.pickledState = self.history.loadStatePickle(self.tid)
+ loadedState = self.history.loadState(self.tid)
+ except Exception as e:
+ self.loadError = "%s: %s" % (e.__class__.__name__, e)
+ self.state = LoadErrorState(self.loadError, self.requestedTid)
+ else:
+ request = check_request()
+ self.state = request.registry.getMultiAdapter((self.obj, loadedState, self.requestedTid),
+ IStateInterpreter)
+
+ def getError(self):
+ return self.loadError
+
+ def listAttributes(self):
+ return self.state.listAttributes()
+
+ def listItems(self):
+ return self.state.listItems()
+
+ def getParent(self):
+ return self.state.getParent()
+
+ def getName(self):
+ name = self.state.getName()
+ if name is None:
+ # __name__ is not in the pickled state, but it may be defined
+ # via other means (e.g. class attributes, custom __getattr__ etc.)
+ try:
+ name = getattr(self.obj, '__name__', None)
+ except Exception:
+ # Ouch. Oh well, we can't determine the name.
+ pass
+ return name
+
+ def asDict(self):
+ return self.state.asDict()
+
+ # These are not part of IStateInterpreter
+
+ def getObjectId(self):
+ return u64(self.obj._p_oid)
+
+ def isRoot(self):
+ return IContainmentRoot.providedBy(self.obj)
+
+ def getParentState(self):
+ parent = self.getParent()
+ if parent is None:
+ return None
+ else:
+ return ZodbObjectState(parent, self.requestedTid)
+
+
+@implementer(IStateInterpreter)
+class LoadErrorState(object):
+ """Placeholder for when an object's state could not be loaded"""
+
+ def __init__(self, error, tid):
+ self.error = error
+ self.tid = tid
+
+ def getError(self):
+ return self.error
+
+ def getName(self):
+ return None
+
+ def getParent(self):
+ return None
+
+ def listAttributes(self):
+ return []
+
+ def listItems(self):
+ return None
+
+ def asDict(self):
+ return {}
+
+
+@adapter_config(context=(Interface, dict, None), provides=IStateInterpreter)
+@implementer(IStateInterpreter)
+class GenericState(object):
+ """Most persistent objects represent their state as a dict."""
+
+ def __init__(self, type, state, tid):
+ self.state = state
+ self.tid = tid
+
+ def getError(self):
+ return None
+
+ def getName(self):
+ return self.state.get('__name__')
+
+ def getParent(self):
+ return self.state.get('__parent__')
+
+ def listAttributes(self):
+ return list(self.state.items())
+
+ def listItems(self):
+ return None
+
+ def asDict(self):
+ return self.state
+
+
+@adapter_config(context=(PersistentMapping, dict, None), provides=IStateInterpreter)
+class PersistentMappingState(GenericState):
+ """Convenient access to a persistent mapping's items."""
+
+ def listItems(self):
+ return sorted(self.state.get('data', {}).items())
+
+
+if PersistentMapping is PersistentDict:
+ # ZODB 3.9 deprecated PersistentDict and made it an alias for
+ # PersistentMapping. I don't know a clean way to conditionally disable the
+ # <adapter> directive in ZCML to avoid conflicting configuration actions,
+ # therefore I'll register a decoy adapter registered for a decoy class.
+ # This adapter will never get used.
+
+ class DecoyPersistentDict(PersistentMapping):
+ """Decoy to avoid ZCML errors while supporting both ZODB 3.8 and 3.9."""
+
+ @adapter_config(context=(DecoyPersistentDict, dict, None), provides=IStateInterpreter)
+ class PersistentDictState(PersistentMappingState):
+ """Decoy to avoid ZCML errors while supporting both ZODB 3.8 and 3.9."""
+
+else:
+
+ @adapter_config(context=(PersistentDict, dict, None), provides=IStateInterpreter)
+ class PersistentDictState(PersistentMappingState):
+ """Convenient access to a persistent dict's items."""
+
+
+@adapter_config(context=(SampleContainer, dict, None), provides=IStateInterpreter)
+class SampleContainerState(GenericState):
+ """Convenient access to a SampleContainer's items"""
+
+ def listItems(self):
+ data = self.state.get('_SampleContainer__data')
+ if not data:
+ return []
+ # data will be something persistent, maybe a PersistentDict, maybe a
+ # OOBTree -- SampleContainer itself uses a plain Python dict, but
+ # subclasses are supposed to overwrite the _newContainerData() method
+ # and use something persistent.
+ loadedstate = IObjectHistory(data).loadState(self.tid)
+ request = check_request()
+ return request.registry.getMultiAdapter((data, loadedstate, self.tid),
+ IStateInterpreter).listItems()
+
+
+@adapter_config(context=(OrderedContainer, dict, None), provides=IStateInterpreter)
+class OrderedContainerState(GenericState):
+ """Convenient access to an OrderedContainer's items"""
+
+ def listItems(self):
+ # Now this is tricky: we want to construct a small object graph using
+ # old state pickles without ever calling __setstate__ on a real
+ # Persistent object, as _that_ would poison ZODB in-memory caches
+ # in a nasty way (LP #487243).
+ container = OrderedContainer()
+ container.__setstate__(self.state)
+ if isinstance(container._data, PersistentDict):
+ old_data_state = IObjectHistory(container._data).loadState(self.tid)
+ container._data = PersistentDict()
+ container._data.__setstate__(old_data_state)
+ if isinstance(container._order, PersistentList):
+ old_order_state = IObjectHistory(container._order).loadState(self.tid)
+ container._order = PersistentList()
+ container._order.__setstate__(old_order_state)
+ return list(container.items())
+
+
+@adapter_config(context=(ContainedProxy, tuple, None), provides=IStateInterpreter)
+class ContainedProxyState(GenericState):
+
+ def __init__(self, proxy, state, tid):
+ GenericState.__init__(self, proxy, state, tid)
+ self.proxy = proxy
+
+ def getName(self):
+ return self.state[1]
+
+ def getParent(self):
+ return self.state[0]
+
+ def listAttributes(self):
+ return [('__name__', self.getName()),
+ ('__parent__', self.getParent()),
+ ('proxied_object', self.proxy.__getnewargs__()[0])]
+
+ def listItems(self):
+ return []
+
+ def asDict(self):
+ return dict(self.listAttributes())
+
+
+@adapter_config(context=(Interface, Interface, None), provides=IStateInterpreter)
+@implementer(IStateInterpreter)
+class FallbackState(object):
+ """Fallback when we've got no idea how to interpret the state"""
+
+ def __init__(self, type, state, tid):
+ self.state = state
+
+ def getError(self):
+ return None
+
+ def getName(self):
+ return None
+
+ def getParent(self):
+ return None
+
+ def listAttributes(self):
+ return [('pickled state', self.state)]
+
+ def listItems(self):
+ return None
+
+ def asDict(self):
+ return dict(self.listAttributes())
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zodbbrowser/tests/__init__.py Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,1 @@
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zodbbrowser/tests/test_utilsdocs.py Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,59 @@
+#
+# 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.
+#
+
+"""
+Generic Test case for pyams_zodbbrowser doctest
+"""
+__docformat__ = 'restructuredtext'
+
+import unittest
+import doctest
+import sys
+import os
+
+
+current_dir = os.path.dirname(__file__)
+
+def doc_suite(test_dir, setUp=None, tearDown=None, globs=None):
+ """Returns a test suite, based on doctests found in /doctest."""
+ suite = []
+ if globs is None:
+ globs = globals()
+
+ flags = (doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE |
+ doctest.REPORT_ONLY_FIRST_FAILURE)
+
+ package_dir = os.path.split(test_dir)[0]
+ if package_dir not in sys.path:
+ sys.path.append(package_dir)
+
+ doctest_dir = os.path.join(package_dir, 'doctests')
+
+ # filtering files on extension
+ docs = [os.path.join(doctest_dir, doc) for doc in
+ os.listdir(doctest_dir) if doc.endswith('.txt')]
+
+ for test in docs:
+ suite.append(doctest.DocFileSuite(test, optionflags=flags,
+ globs=globs, setUp=setUp,
+ tearDown=tearDown,
+ module_relative=False))
+
+ return unittest.TestSuite(suite)
+
+def test_suite():
+ """returns the test suite"""
+ return doc_suite(current_dir)
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zodbbrowser/tests/test_utilsdocstrings.py Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,62 @@
+#
+# 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.
+#
+
+"""
+Generic Test case for pyams_zodbbrowser doc strings
+"""
+__docformat__ = 'restructuredtext'
+
+import unittest
+import doctest
+import sys
+import os
+
+
+current_dir = os.path.abspath(os.path.dirname(__file__))
+
+def doc_suite(test_dir, globs=None):
+ """Returns a test suite, based on doc tests strings found in /*.py"""
+ suite = []
+ if globs is None:
+ globs = globals()
+
+ flags = (doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE |
+ doctest.REPORT_ONLY_FIRST_FAILURE)
+
+ package_dir = os.path.split(test_dir)[0]
+ if package_dir not in sys.path:
+ sys.path.append(package_dir)
+
+ # filtering files on extension
+ docs = [doc for doc in
+ os.listdir(package_dir) if doc.endswith('.py')]
+ docs = [doc for doc in docs if not doc.startswith('__')]
+
+ for test in docs:
+ fd = open(os.path.join(package_dir, test))
+ content = fd.read()
+ fd.close()
+ if '>>> ' not in content:
+ continue
+ test = test.replace('.py', '')
+ location = 'pyams_zodbbrowser.%s' % test
+ suite.append(doctest.DocTestSuite(location, optionflags=flags,
+ globs=globs))
+
+ return unittest.TestSuite(suite)
+
+def test_suite():
+ """returns the test suite"""
+ return doc_suite(current_dir)
+
+if __name__ == '__main__':
+ unittest.main(defaultTest='test_suite')
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zodbbrowser/value.py Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,304 @@
+#
+# 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 logging
+import itertools
+import collections
+import re
+from html import escape
+
+# import interfaces
+from pyams_zodbbrowser.interfaces import IValueRenderer, IObjectHistory
+
+# import packages
+from persistent import Persistent
+from persistent.dict import PersistentDict
+from persistent.list import PersistentList
+from persistent.mapping import PersistentMapping
+from pyams_utils.adapter import adapter_config
+from ZODB.utils import u64, oid_repr
+from zope.interface import implementer, Interface
+from zope.interface.declarations import ProvidesClass
+
+
+log = logging.getLogger(__name__)
+
+
+MAX_CACHE_SIZE = 1000
+TRUNCATIONS = {}
+TRUNCATIONS_IN_ORDER = collections.deque()
+next_id = itertools.count(1).__next__
+
+
+def resetTruncations(): # for tests only!
+ global next_id
+ next_id = itertools.count(1).__next__
+ TRUNCATIONS.clear()
+ TRUNCATIONS_IN_ORDER.clear()
+
+
+def pruneTruncations():
+ while len(TRUNCATIONS_IN_ORDER) > MAX_CACHE_SIZE:
+ del TRUNCATIONS[TRUNCATIONS_IN_ORDER.popleft()]
+
+
+def truncate(text):
+ id = 'tr%d' % next_id()
+ TRUNCATIONS[id] = text
+ TRUNCATIONS_IN_ORDER.append(id)
+ return id
+
+
+@adapter_config(context=Interface, provides=IValueRenderer)
+@implementer(IValueRenderer)
+class GenericValue(object):
+ """Default value renderer.
+
+ Uses the object's __repr__, truncating if too long.
+ """
+
+ def __init__(self, context):
+ self.context = context
+
+ def _repr(self):
+ # hook for subclasses
+ if getattr(self.context.__class__, '__repr__', None) is object.__repr__:
+ # Special-case objects with the default __repr__ (LP#1087138)
+ if isinstance(self.context, Persistent):
+ return '<%s.%s with oid %s>' % (
+ self.context.__class__.__module__,
+ self.context.__class__.__name__,
+ oid_repr(self.context._p_oid))
+ try:
+ return repr(self.context)
+ except Exception:
+ try:
+ return '<unrepresentable %s>' % self.context.__class__.__name__
+ except Exception:
+ return '<unrepresentable>'
+
+ def render(self, tid=None, can_link=True, limit=200):
+ text = self._repr()
+ if len(text) > limit:
+ id = truncate(text[limit:])
+ text = '%s<span id="%s" class="truncated">...</span>' % (
+ escape(text[:limit]), id)
+ else:
+ text = escape(text)
+ if not isinstance(self.context, str):
+ try:
+ n = len(self.context)
+ except Exception:
+ pass
+ else:
+ if n == 1: # this is a crime against i18n, but oh well
+ text += ' (%d item)' % n
+ else:
+ text += ' (%d items)' % n
+ return text
+
+
+def join_with_commas(html, open, close):
+ """Helper to join multiple html snippets into a struct."""
+ prefix = open + '<span class="struct">'
+ suffix = '</span>'
+ for n, item in enumerate(html):
+ if n == len(html) - 1:
+ trailer = close
+ else:
+ trailer = ','
+ if item.endswith(suffix):
+ item = item[:-len(suffix)] + trailer + suffix
+ else:
+ item += trailer
+ html[n] = item
+ return prefix + '<br />'.join(html) + suffix
+
+
+@adapter_config(context=str, provides=IValueRenderer)
+class StringValue(GenericValue):
+ """String renderer."""
+
+ def __init__(self, context):
+ self.context = context
+
+ def render(self, tid=None, can_link=True, limit=200, threshold=4):
+ if self.context.count('\n') <= threshold:
+ return GenericValue.render(self, tid, can_link=can_link,
+ limit=limit)
+ else:
+ if isinstance(self.context, str):
+ prefix = 'u'
+ context = self.context
+ else:
+ prefix = ''
+ context = self.context.decode('latin-1').encode('ascii',
+ 'backslashreplace')
+ lines = [re.sub(r'^[ \t]+',
+ lambda m: ' ' * len(m.group(0).expandtabs()),
+ escape(line))
+ for line in context.splitlines()]
+ nl = '<br />' # hm, maybe '\\n<br />'?
+ if sum(map(len, lines)) > limit:
+ head = nl.join(lines[:5])
+ tail = nl.join(lines[5:])
+ id = truncate(tail)
+ return (prefix + "'<span class=\"struct\">" + head + nl
+ + '<span id="%s" class="truncated">...</span>' % id
+ + "'</span>")
+ else:
+ return (prefix + "'<span class=\"struct\">" + nl.join(lines)
+ + "'</span>")
+
+
+@adapter_config(context=tuple, provides=IValueRenderer)
+@implementer(IValueRenderer)
+class TupleValue(object):
+ """Tuple renderer."""
+
+ def __init__(self, context):
+ self.context = context
+
+ def render(self, tid=None, can_link=True, threshold=100):
+ html = []
+ for item in self.context:
+ html.append(IValueRenderer(item).render(tid, can_link))
+ if len(html) == 1:
+ html.append('') # (item) -> (item, )
+ result = '(%s)' % ', '.join(html)
+ if len(result) > threshold or '<span class="struct">' in result:
+ if len(html) == 2 and html[1] == '':
+ return join_with_commas(html[:1], '(', ', )')
+ else:
+ return join_with_commas(html, '(', ')')
+ return result
+
+
+@adapter_config(context=list, provides=IValueRenderer)
+@implementer(IValueRenderer)
+class ListValue(object):
+ """List renderer."""
+
+ def __init__(self, context):
+ self.context = context
+
+ def render(self, tid=None, can_link=True, threshold=100):
+ html = []
+ for item in self.context:
+ html.append(IValueRenderer(item).render(tid, can_link))
+ result = '[%s]' % ', '.join(html)
+ if len(result) > threshold or '<span class="struct">' in result:
+ return join_with_commas(html, '[', ']')
+ return result
+
+
+@adapter_config(context=dict, provides=IValueRenderer)
+@implementer(IValueRenderer)
+class DictValue(object):
+ """Dict renderer."""
+
+ def __init__(self, context):
+ self.context = context
+
+ def render(self, tid=None, can_link=True, threshold=100):
+ html = []
+ for key, value in sorted(self.context.items()):
+ html.append(IValueRenderer(key).render(tid, can_link) + ': ' +
+ IValueRenderer(value).render(tid, can_link))
+ if (sum(map(len, html)) < threshold and
+ '<span class="struct">' not in ''.join(html)):
+ return '{%s}' % ', '.join(html)
+ else:
+ return join_with_commas(html, '{', '}')
+
+
+@adapter_config(context=Persistent, provides=IValueRenderer)
+@implementer(IValueRenderer)
+class PersistentValue(object):
+ """Persistent object renderer.
+
+ Uses __repr__ and makes it a hyperlink to the actual object.
+ """
+
+ view_name = '#zodbbrowser'
+ delegate_to = GenericValue
+
+ def __init__(self, context):
+ self.context = context
+
+ def render(self, tid=None, can_link=True):
+ obj = self.context
+ url = '%s?oid=0x%x' % (self.view_name, u64(self.context._p_oid))
+ if tid is not None:
+ url += "&tid=%d" % u64(tid)
+ try:
+ oldstate = IObjectHistory(self.context).loadState(tid)
+ clone = self.context.__class__.__new__(self.context.__class__)
+ clone.__setstate__(oldstate)
+ clone._p_oid = self.context._p_oid
+ obj = clone
+ except Exception:
+ log.debug('Could not load old state for %s 0x%x',
+ self.context.__class__, u64(self.context._p_oid))
+ value = self.delegate_to(obj).render(tid, can_link=False)
+ if can_link:
+ return '<a class="objlink" href="%s">%s</a>' % (escape(url), value)
+ else:
+ return value
+
+
+@adapter_config(context=PersistentMapping, provides=IValueRenderer)
+class PersistentMappingValue(PersistentValue):
+ delegate_to = DictValue
+
+
+@adapter_config(context=PersistentList, provides=IValueRenderer)
+class PersistentListValue(PersistentValue):
+ delegate_to = ListValue
+
+
+if PersistentMapping is PersistentDict:
+ # ZODB 3.9 deprecated PersistentDict and made it an alias for
+ # PersistentMapping. I don't know a clean way to conditionally disable the
+ # <adapter> directive in ZCML to avoid conflicting configuration actions,
+ # therefore I'll register a decoy adapter registered for a decoy class.
+ # This adapter will never get used.
+
+ class DecoyPersistentDict(PersistentMapping):
+ """Decoy to avoid ZCML errors while supporting both ZODB 3.8 and 3.9."""
+
+ @adapter_config(context=DecoyPersistentDict, provides=IValueRenderer)
+ class PersistentDictValue(PersistentValue):
+ """Decoy to avoid ZCML errors while supporting both ZODB 3.8 and 3.9."""
+ delegate_to = DictValue
+
+else:
+ @adapter_config(context=PersistentDict, provides=IValueRenderer)
+ class PersistentDictValue(PersistentValue):
+ delegate_to = DictValue
+
+
+@adapter_config(context=ProvidesClass, provides=IValueRenderer)
+class ProvidesValue(GenericValue):
+ """zope.interface.Provides object renderer.
+
+ The __repr__ of zope.interface.Provides is decidedly unhelpful.
+ """
+
+ def _repr(self):
+ return '<Provides: %s>' % ', '.join(i.__identifier__
+ for i in self.context._Provides__args[1:])
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zodbbrowser/zmi/__init__.py Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,19 @@
+#
+# 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 interfaces
+
+# import packages
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zodbbrowser/zmi/resources/css/zodbbrowser.css Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,258 @@
+div.zodbbrowser {
+ color: black;
+ background-color: white;
+ border: 1px solid #00cc33;
+ min-width: 600px;
+ margin-top: 5px;
+ font-size: 12px;
+ font-weight: normal;
+}
+
+div.zodbbrowser > div {
+ padding: 5px;
+}
+
+div.heading {
+ background-color: #aaffaa;
+ border-bottom: 1px solid #00cc33;
+ padding: 5px;
+}
+
+div.heading h1 {
+ font-size: 16px;
+ color: #0033cc;
+ margin: 0;
+ margin-right: 4px;
+}
+div.heading h1 a:link,
+div.heading h1 a:visited {
+ color: #0033cc;
+ text-decoration: none;
+}
+div.heading h1 a:hover,
+div.heading h1 a:active {
+ color: white;
+}
+
+#path {
+ display: block;
+ border: 1px solid #afa;
+ padding: 1px 2px;
+}
+
+#path:hover {
+ border: 1px solid #00cc33;
+ cursor: default;
+}
+
+input#gotoInput {
+ display: block;
+ width: 100%;
+ border: 1px solid #00cc33;
+ background: white;
+ color: black;
+ font-size: 16px;
+ margin: 0;
+ padding: 1px;
+ font-family: Tahoma, Verdana, sans-serif;
+ font-weight: bold;
+}
+
+#pathError {
+ display: block;
+ font-size: 14px;
+ color: black;
+ font-weight: normal;
+ margin: 6px;
+}
+
+div.zodbbrowser .pickleSize {
+ float: right;
+}
+
+div.zodbbrowser .pickleSize,
+div.zodbbrowser h2 {
+ font-size: 14px;
+ color: black;
+ margin: 2px;
+ font-weight: normal;
+}
+
+div.zodbbrowser h3 {
+ font-size: 14px;
+ color: orange;
+ margin: 2px;
+}
+
+h3.expander {
+ cursor: default;
+ padding: 1px;
+}
+
+h3.expander:hover {
+ background: #f8fff8;
+}
+
+h5.expander {
+ cursor: default;
+ padding: 1px;
+}
+
+h5.expander:hover {
+ background: #f8fff8;
+}
+
+
+div.transaction {
+ margin: 0;
+ padding-bottom: 1em;
+}
+
+h4.transaction {
+ margin: 0;
+ font-size: 14px;
+ font-weight: bold;
+ color: black;
+}
+
+h4.transaction.paging {
+ text-align: right;
+}
+
+h4.transaction a.title:link,
+h4.transaction a.title:visited {
+ display: block;
+ color: black;
+ text-decoration: none;
+ padding: 2px;
+}
+h4.transaction a.title:hover,
+h4.transaction a.title:active {
+ color: #760;
+ background: #ffa;
+}
+
+h4.transaction a.subtitle {
+ float: right;
+ padding: 2px;
+ color: #003344;
+ font-size: 12px;
+ font-weight: normal;
+}
+
+div.transaction div.toolbox {
+ float: right;
+ padding: 4px;
+}
+
+div.transaction div.toolbox {
+ display: none;
+}
+
+div.transaction:hover div.toolbox {
+ display: block;
+}
+
+div.zodbbrowser .truncated {
+ background: #f44;
+ color: white;
+ margin: 2px;
+ padding: 0 1px;
+ border-radius: 2px;
+ -moz-border-radius: 2px;
+ cursor: pointer;
+}
+
+span.struct {
+ display: inline-block;
+ vertical-align: text-top;
+}
+
+a.objlink {
+ display: inline-block;
+ vertical-align: text-top;
+}
+
+a.objlink:hover,
+a.objlink:active {
+ background: #aaf;
+ color: white;
+ text-decoration: none;
+}
+
+.tid-info {
+ color: #141;
+ margin-left: 2em;
+ padding: 1px;
+ background: #ffa;
+ border-bottom: 1px solid #fe0;
+ border-top: 1px solid #fe0;
+ border-left: 1px solid #fe0;
+ border-right: 1px solid #fe0;
+}
+
+div.error,
+div.diff {
+ padding: 4px;
+ margin-left: 2em;
+ margin-bottom: 0;
+}
+
+div.diff ol {
+ padding-top: 0;
+ padding-bottom: 0;
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+.removed {
+ text-decoration: line-through;
+ color: #400;
+}
+
+.added {
+ color: #040;
+}
+
+div.filtered {
+ margin-left: 2em;
+ padding-left: 4px;
+ font-size: 12px;
+ color: #444;
+}
+
+div.buttons {
+ padding: 4px;
+}
+a.jsbutton:link,
+a.jsbutton:visited {
+ color: green;
+ padding: 1px 4px;
+}
+
+div.history {
+}
+
+div.current {
+ background: #ffa;
+ border-bottom: 1px solid #fe0;
+ border-top: 1px solid #fe0;
+}
+div.latest {
+ background: #ffa;
+ border-top: 1px solid #fe0;
+ margin-bottom: 0;
+}
+div.focus {
+ background: #fca;
+ border-bottom: 1px solid #f80;
+ border-top: 1px solid #f80;
+}
+div#confirmation {
+ margin-left: 2em;
+ margin-top: 1em;
+ font-weight: bold;
+}
+div#confirmation div.message {
+ margin-bottom: 1ex;
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zodbbrowser/zmi/resources/css/zodbbrowser.min.css Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,1 @@
+div.zodbbrowser{color:black;background-color:white;border:1px solid #0c3;min-width:600px;margin-top:5px;font-size:12px;font-weight:normal}div.zodbbrowser>div{padding:5px}div.heading{background-color:#afa;border-bottom:1px solid #0c3;padding:5px}div.heading h1{font-size:16px;color:#03c;margin:0;margin-right:4px}div.heading h1 a:link,div.heading h1 a:visited{color:#03c;text-decoration:none}div.heading h1 a:hover,div.heading h1 a:active{color:white}#path{display:block;border:1px solid #afa;padding:1px 2px}#path:hover{border:1px solid #0c3;cursor:default}input#gotoInput{display:block;width:100%;border:1px solid #0c3;background:white;color:black;font-size:16px;margin:0;padding:1px;font-family:Tahoma,Verdana,sans-serif;font-weight:bold}#pathError{display:block;font-size:14px;color:black;font-weight:normal;margin:6px}div.zodbbrowser .pickleSize{float:right}div.zodbbrowser .pickleSize,div.zodbbrowser h2{font-size:14px;color:black;margin:2px;font-weight:normal}div.zodbbrowser h3{font-size:14px;color:orange;margin:2px}h3.expander{cursor:default;padding:1px}h3.expander:hover{background:#f8fff8}h5.expander{cursor:default;padding:1px}h5.expander:hover{background:#f8fff8}div.transaction{margin:0;padding-bottom:1em}h4.transaction{margin:0;font-size:14px;font-weight:bold;color:black}h4.transaction.paging{text-align:right}h4.transaction a.title:link,h4.transaction a.title:visited{display:block;color:black;text-decoration:none;padding:2px}h4.transaction a.title:hover,h4.transaction a.title:active{color:#760;background:#ffa}h4.transaction a.subtitle{float:right;padding:2px;color:#034;font-size:12px;font-weight:normal}div.transaction div.toolbox{float:right;padding:4px}div.transaction div.toolbox{display:none}div.transaction:hover div.toolbox{display:block}div.zodbbrowser .truncated{background:#f44;color:white;margin:2px;padding:0 1px;border-radius:2px;-moz-border-radius:2px;cursor:pointer}span.struct{display:inline-block;vertical-align:text-top}a.objlink{display:inline-block;vertical-align:text-top}a.objlink:hover,a.objlink:active{background:#aaf;color:white;text-decoration:none}.tid-info{color:#141;margin-left:2em;padding:1px;background:#ffa;border-bottom:1px solid #fe0;border-top:1px solid #fe0;border-left:1px solid #fe0;border-right:1px solid #fe0}div.error,div.diff{padding:4px;margin-left:2em;margin-bottom:0}div.diff ol{padding-top:0;padding-bottom:0;margin-top:0;margin-bottom:0}.removed{text-decoration:line-through;color:#400}.added{color:#040}div.filtered{margin-left:2em;padding-left:4px;font-size:12px;color:#444}div.buttons{padding:4px}a.jsbutton:link,a.jsbutton:visited{color:green;padding:1px 4px}div.current{background:#ffa;border-bottom:1px solid #fe0;border-top:1px solid #fe0}div.latest{background:#ffa;border-top:1px solid #fe0;margin-bottom:0}div.focus{background:#fca;border-bottom:1px solid #f80;border-top:1px solid #f80}div#confirmation{margin-left:2em;margin-top:1em;font-weight:bold}div#confirmation div.message{margin-bottom:1ex}
\ No newline at end of file
Binary file src/pyams_zodbbrowser/zmi/resources/img/collapse.png has changed
Binary file src/pyams_zodbbrowser/zmi/resources/img/expand.png has changed
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zodbbrowser/zmi/resources/js/zodbbrowser.js Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,221 @@
+(function($) {
+
+ window.PyAMS_zodbbrowser = {
+
+ filterAll: function () {
+ $('.filter').attr('checked', true);
+ PyAMS_zodbbrowser.filterHistory(true);
+ },
+
+ filterNone: function () {
+ $('.filter').attr('checked', false);
+ PyAMS_zodbbrowser.filterHistory();
+ },
+
+ filterHistory: function (showAll) {
+ var transactions = $('div.transaction');
+ var filters = $('.filter');
+ var filterMap = Array();
+ var MAPPING_PREFIX = "map";
+
+ for (var i = 0; i < filters.length; i++) {
+ var filter = filters[i];
+ if (filter.checked) {
+ filterMap[MAPPING_PREFIX + filter.name] = filter.checked;
+ }
+ }
+
+ for (i = 0; i < transactions.length; i++) {
+ var transaction = transactions[i];
+ var diffs = $(transaction).children('div.diff').children('div.diffitem');
+ var n_hidden = 0;
+ for (var j = 0; j < diffs.length; j++) {
+ var id = $($(diffs[j]).children()[0]).text();
+ if (MAPPING_PREFIX + id in filterMap || showAll) {
+ $(diffs[j]).show();
+ } else {
+ $(diffs[j]).hide();
+ n_hidden += 1;
+ }
+ }
+ var hidden_text = null;
+ if (n_hidden == 1) {
+ hidden_text = '1 item hidden';
+ } else if (n_hidden) {
+ hidden_text = n_hidden + ' items hidden';
+ }
+ $(transaction).children('.filtered').remove();
+ if (hidden_text) {
+ $(transaction).append('<div class="filtered">' + hidden_text + '</div>');
+ }
+ }
+ },
+
+ collapseOrExpand: function () {
+ // this is <div class="extender">
+ // the following element is <div class="collapsible">
+ var content = $(this).next();
+ var icon = $(this).children('img');
+ if (content.is(':hidden')) {
+ $(icon).attr('src', $('#collapseImg').attr('src'));
+ content.slideDown();
+ } else {
+ $(icon).attr('src', $('#expandImg').attr('src'));
+ content.slideUp();
+ }
+ },
+
+ hideItemsIfTooMany: function () {
+ $('.items').each(function () {
+ var expander = $(this).children('.expander')[0];
+ var content = $(this).children('.collapsible')[0];
+ // items are formatted using <br /> so the heuristic is very
+ // approximate.
+ if (content.childNodes.length > 100 && !$(content).is(':hidden')) {
+ var icon = $(expander).children('img');
+ $(icon).attr('src', $('#expandImg').attr('src'));
+ $(content).hide();
+ }
+ });
+ },
+
+ showGoTo: function () {
+ $('#path').hide();
+ $('#goto').show();
+ $('#gotoInput').focus();
+ },
+
+ hideGoTo: function () {
+ $('#goto').hide();
+ $('#path').show();
+ // Don't hide #pathError right away, this blur event might have been the
+ // result of the user clicking on a link inside the #pathError text and
+ // hiding it will prevent that link from getting activated.
+ setTimeout(function () {
+ $('#pathError').slideUp();
+ }, 50);
+ },
+
+ ajaxErrorHandler: function (XMLHttpRequest, textStatus, errorThrown) {
+ errorMessage = "";
+ if (textStatus == "parsererror") {
+ errorMessage = "Server returned malformed data";
+ } else if (textStatus == "error") {
+ errorMessage = "Unknown error (maybe server is offline?)";
+ } else if (textStatus == "timeout") {
+ errorMessage = "Server timeout";
+ } else if (textStatus == "notmodified") {
+ errorMessage = "Server error (says resource not modified)";
+ } else {
+ errorMessage = "Unknown error";
+ }
+
+ errorMessage = '<span class="error"> ' + errorMessage + '</strong>';
+ $('#pathError').html(errorMessage);
+ },
+
+ ajaxSuccessHandler: function (data, status) {
+ if (data.url) {
+ window.location = data.url;
+ $('#pathError').text("Found.").slideDown().slideUp();
+ } else if (data.error) {
+ $('#pathError').text(data.error).show();
+ if (data.partial_url) {
+ $('#pathError').append(', would you like to ' +
+ '<a href="' + data.partial_url + '">' +
+ 'go to ' + data.partial_path +
+ '</a>' +
+ ' instead?');
+ }
+ } else {
+ $('#pathError').text(status).show();
+ }
+ },
+
+ activateGoTo: function () {
+ var path = $('#gotoInput').val();
+ var api_url = 'zodbbrowser_path_to_oid';
+ $('#pathError').text("Loading...").slideDown();
+ $.ajax({
+ url: api_url,
+ dataType: 'json',
+ data: "path=" + path,
+ timeout: 7000,
+ success: PyAMS_zodbbrowser.ajaxSuccessHandler,
+ error: PyAMS_zodbbrowser.ajaxErrorHandler
+ });
+ },
+
+ cancelRollback: function (e) {
+ $('#confirmation').remove();
+ $('div.transaction').removeClass('focus');
+ $('input.rollbackbtn').show();
+ },
+
+ pressRollback: function (e) {
+ e.preventDefault();
+ PyAMS_zodbbrowser.cancelRollback();
+ $(e.target).hide();
+ var transaction_div = $(e.target).closest('div.transaction');
+ transaction_div.addClass('focus');
+ $('<div id="confirmation">' +
+ '<form action="" method="post">' +
+ '<div class="message">' +
+ 'This is a dangerous operation that may break data integrity.' +
+ ' Are you really sure you want to do this?' +
+ '</div>' +
+ '<input type="BUTTON" value="Really revert to this state" onclick="PyAMS_zodbbrowser.doRollback()">' +
+ '<input type="BUTTON" value="Cancel" onclick="PyAMS_zodbbrowser.cancelRollback()">' +
+ '</form>' +
+ '</div>').appendTo(transaction_div);
+ },
+
+ doRollback: function () {
+ var transaction_div = $('#confirmation').closest('div.transaction');
+ var rollback_form = transaction_div.find('form.rollback');
+ rollback_form.find('input[name="confirmed"]').val('1');
+ rollback_form.submit();
+ },
+
+ init: function(element) {
+ $('.expander', element).click(PyAMS_zodbbrowser.collapseOrExpand);
+ PyAMS_zodbbrowser.hideItemsIfTooMany();
+ $('#path a', element).click(function (event) {
+ event.stopPropagation();
+ });
+ $('#path', element).click(PyAMS_zodbbrowser.showGoTo);
+ $('#gotoInput', element).blur(PyAMS_zodbbrowser.hideGoTo);
+ $('#gotoInput', element).keypress(function (event) {
+ if (event.which == 13) { // enter
+ PyAMS_zodbbrowser.activateGoTo();
+ }
+ });
+ $('#gotoInput', element).keydown(function (event) {
+ if (event.keyCode == 27) { // escape
+ PyAMS_zodbbrowser.hideGoTo();
+ }
+ });
+ $(document).keypress(function (event) {
+ if (event.which == 103) { // lowercase g
+ if ($('#goto', element).is(':hidden')) {
+ PyAMS_zodbbrowser.showGoTo();
+ event.preventDefault();
+ }
+ }
+ });
+ $('input.rollbackbtn', element).click(PyAMS_zodbbrowser.pressRollback);
+ $('span.truncated', element).click(function (event) {
+ event.preventDefault();
+ var placeholder = $(this);
+ var id = placeholder.attr('id');
+ $.ajax({
+ url: 'zodbbrowser_truncated', data: 'id=' + id,
+ success: function (data, status) {
+ placeholder.replaceWith(data);
+ }
+ });
+ });
+ }
+ }
+
+})(jQuery);
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zodbbrowser/zmi/resources/js/zodbbrowser.min.js Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,1 @@
+(function(a){window.PyAMS_zodbbrowser={filterAll:function(){a(".filter").attr("checked",true);PyAMS_zodbbrowser.filterHistory(true)},filterNone:function(){a(".filter").attr("checked",false);PyAMS_zodbbrowser.filterHistory()},filterHistory:function(k){var p=a("div.transaction");var g=a(".filter");var b=Array();var c="map";for(var n=0;n<g.length;n++){var e=g[n];if(e.checked){b[c+e.name]=e.checked}}for(n=0;n<p.length;n++){var h=p[n];var o=a(h).children("div.diff").children("div.diffitem");var f=0;for(var m=0;m<o.length;m++){var d=a(a(o[m]).children()[0]).text();if(c+d in b||k){a(o[m]).show()}else{a(o[m]).hide();f+=1}}var l=null;if(f==1){l="1 item hidden"}else{if(f){l=f+" items hidden"}}a(h).children(".filtered").remove();if(l){a(h).append('<div class="filtered">'+l+"</div>")}}},collapseOrExpand:function(){var c=a(this).next();var b=a(this).children("img");if(c.is(":hidden")){a(b).attr("src",a("#collapseImg").attr("src"));c.slideDown()}else{a(b).attr("src",a("#expandImg").attr("src"));c.slideUp()}},hideItemsIfTooMany:function(){a(".items").each(function(){var d=a(this).children(".expander")[0];var c=a(this).children(".collapsible")[0];if(c.childNodes.length>100&&!a(c).is(":hidden")){var b=a(d).children("img");a(b).attr("src",a("#expandImg").attr("src"));a(c).hide()}})},showGoTo:function(){a("#path").hide();a("#goto").show();a("#gotoInput").focus()},hideGoTo:function(){a("#goto").hide();a("#path").show();setTimeout(function(){a("#pathError").slideUp()},50)},ajaxErrorHandler:function(b,d,c){errorMessage="";if(d=="parsererror"){errorMessage="Server returned malformed data"}else{if(d=="error"){errorMessage="Unknown error (maybe server is offline?)"}else{if(d=="timeout"){errorMessage="Server timeout"}else{if(d=="notmodified"){errorMessage="Server error (says resource not modified)"}else{errorMessage="Unknown error"}}}}errorMessage='<span class="error"> '+errorMessage+"</strong>";a("#pathError").html(errorMessage)},ajaxSuccessHandler:function(c,b){if(c.url){window.location=c.url;a("#pathError").text("Found.").slideDown().slideUp()}else{if(c.error){a("#pathError").text(c.error).show();if(c.partial_url){a("#pathError").append(', would you like to <a href="'+c.partial_url+'">go to '+c.partial_path+"</a> instead?")}}else{a("#pathError").text(b).show()}}},activateGoTo:function(){var c=a("#gotoInput").val();var b="zodbbrowser_path_to_oid";a("#pathError").text("Loading...").slideDown();a.ajax({url:b,dataType:"json",data:"path="+c,timeout:7000,success:PyAMS_zodbbrowser.ajaxSuccessHandler,error:PyAMS_zodbbrowser.ajaxErrorHandler})},cancelRollback:function(b){a("#confirmation").remove();a("div.transaction").removeClass("focus");a("input.rollbackbtn").show()},pressRollback:function(c){c.preventDefault();PyAMS_zodbbrowser.cancelRollback();a(c.target).hide();var b=a(c.target).closest("div.transaction");b.addClass("focus");a('<div id="confirmation"><form action="" method="post"><div class="message">This is a dangerous operation that may break data integrity. Are you really sure you want to do this?</div><input type="BUTTON" value="Really revert to this state" onclick="PyAMS_zodbbrowser.doRollback()"><input type="BUTTON" value="Cancel" onclick="PyAMS_zodbbrowser.cancelRollback()"></form></div>').appendTo(b)},doRollback:function(){var b=a("#confirmation").closest("div.transaction");var c=b.find("form.rollback");c.find('input[name="confirmed"]').val("1");c.submit()},init:function(b){a(".expander",b).click(PyAMS_zodbbrowser.collapseOrExpand);PyAMS_zodbbrowser.hideItemsIfTooMany();a("#path a",b).click(function(c){c.stopPropagation()});a("#path",b).click(PyAMS_zodbbrowser.showGoTo);a("#gotoInput",b).blur(PyAMS_zodbbrowser.hideGoTo);a("#gotoInput",b).keypress(function(c){if(c.which==13){PyAMS_zodbbrowser.activateGoTo()}});a("#gotoInput",b).keydown(function(c){if(c.keyCode==27){PyAMS_zodbbrowser.hideGoTo()}});a(document).keypress(function(c){if(c.which==103){if(a("#goto",b).is(":hidden")){PyAMS_zodbbrowser.showGoTo();c.preventDefault()}}});a("input.rollbackbtn",b).click(PyAMS_zodbbrowser.pressRollback);a("span.truncated",b).click(function(c){c.preventDefault();var d=a(this);var e=d.attr("id");a.ajax({url:"zodbbrowser_truncated",data:"id="+e,success:function(g,f){d.replaceWith(g)}})})}}})(jQuery);
\ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zodbbrowser/zmi/templates/zodbhistory.pt Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,78 @@
+<div class="zodbbrowser"
+ data-ams-plugins="zodbbrowser"
+ data-ams-plugin-zodbbrowser-src="/--static--/pyams_zodbbrowser/js/zodbbrowser.js"
+ data-ams-plugin-zodbbrowser-css="/--static--/pyams_zodbbrowser/css/zodbbrowser.css">
+ <div class="heading">
+ <h1 id="path">
+ ZODB transactions
+ </h1>
+
+ <h1 id="goto" style="display:none">
+ <input type="hidden" id="api" name="api" value="#zodbbrowser_path_to_oid" />
+ <input type="text" class="goto" id="gotoInput" name="goto" />
+ </h1>
+ <span id="pathError" style="display:none"></span>
+ </div>
+
+ <div class="history">
+ <div tal:condition="python: view.page > 0">
+ <h4 class="paging transaction" tal:define="prev_page python:view.page - 1">
+ <a class="title" tal:attributes="href string:#zodbbrowser_history?page=${prev_page}">Newer</a>
+ </h4>
+ </div>
+ <div tal:condition="python: view.page == 0"
+ tal:attributes="class python:'tid' in request.params and 'none' or 'latest'">
+ <h4 class="transaction">
+ <a class="title" href="#zodbbrowser_history">Latest</a>
+ </h4>
+ </div>
+ <div class="transaction" tal:repeat="history view.listHistory()"
+ tal:attributes="class python:history['current']
+ and 'transaction current' or 'transaction'">
+ <h4 class="transaction" tal:attributes="id string:tid${history.utid}">
+ <a class="title" tal:attributes="href history.href">
+ #<span tal:replace="history.index" />:
+ <span class="timestamp" tal:content="string:${history.utc_timestamp}" title="UTC" />
+ <span class="user" tal:content="history.user_id"
+ tal:attributes="title string:user from site ${history.user_location}" />
+ <span class="location" tal:content="history.location|nothing"
+ tal:attributes="title string:request type ${history.request_type|nothing}" />
+ <span class="description" tal:content="history.description" />
+ </a>
+ </h4>
+
+ <div class="diff">
+ <h5 class="expander">
+ <img src="/--static--/pyams_zodbbrowser/img/collapse.png"
+ alt="collapse"
+ tal:condition="not:history.hidden"
+ /><img src="/--static--/pyams_zodbbrowser/img/expand.png"
+ alt="expand"
+ tal:condition="history.hidden"
+ /> <span tal:replace="history.summary">N objects saved</span>
+ </h5>
+
+ <div class="collapsible"
+ tal:attributes="style python: history['hidden'] and 'display: none' or None">
+ <ol>
+ <li tal:repeat="obj history.objects">
+ <a tal:attributes="href obj.url" tal:content="obj.path">/(path)</a>
+ <span tal:replace="obj.class_repr" />
+ <a tal:attributes="href obj.url" tal:content="structure obj.repr">(obj)</a>
+ </li>
+ </ol>
+ </div>
+ </div>
+ </div>
+ <div tal:condition="python: view.page < view.last_page">
+ <h4 class="paging transaction" tal:define="next_page python:view.page + 1">
+ <a class="title" tal:attributes="href string:#zodbbrowser_history?page=${next_page}">Older</a>
+ </h4>
+ </div>
+ </div>
+
+</div>
+<img id="collapseImg" style="display:none" alt=""
+ src="/--static--/pyams_zodbbrowser/img/collapse.png" />
+<img id="expandImg" style="display:none" alt=""
+ src="/--static--/pyams_zodbbrowser/img/expand.png" />
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zodbbrowser/zmi/templates/zodbinfo.pt Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,143 @@
+<div class="zodbbrowser"
+ data-ams-plugins="zodbbrowser"
+ data-ams-plugin-zodbbrowser-src="/--static--/pyams_zodbbrowser/js/zodbbrowser.js"
+ data-ams-plugin-zodbbrowser-css="/--static--/pyams_zodbbrowser/css/zodbbrowser.css"
+ data-ams-plugin-zodbbrowser-callback="PyAMS_zodbbrowser.init">
+ <div class="heading">
+ <h1 id="path">
+ <span class="breadcrumbs" tal:content="structure view.getBreadcrumbsHTML()" />
+ </h1>
+
+ <h1 id="goto" style="display:none">
+ <input type="text" class="goto" id="gotoInput" name="goto"
+ tal:attributes="value view.getPath()"/>
+ </h1>
+ <span id="pathError" style="display:none"></span>
+ <span class="pickleSize"> <span tal:replace="view.getPickleSize()" /> bytes pickled</span>
+
+ <h2 class="type" tal:content="view.getObjectType()" />
+
+ <div class="tid-info" tal:condition="not:view.latest">
+ at <a tal:attributes="href string:#zodbbrowser_history?tid=${view.getStateTid()}"
+ tal:content="view.getStateTidNice()"></a>
+ <tal:span tal:content="string:(last change before or at ${view.getRequestedTidNice()})"/>
+ </div>
+ </div>
+
+ <div class="attributes"
+ tal:define="attributes view.listAttributes();
+ error view.state.getError()"
+ tal:condition="python:attributes is not None or error">
+ <h3 class="expander">
+ <img src="/--static--/pyams_zodbbrowser/img/collapse.png"
+ alt="collapse" /> Attributes
+ </h3>
+
+ <div class="collapsible">
+ <tal:block tal:condition="error">
+ <div class="error" tal:condition="error">
+ Could not load the object:
+ <span tal:replace="error" />
+ </div>
+ </tal:block>
+ <tal:block tal:condition="not:attributes">
+ <span tal:condition="not:error" class="empty">There are none.</span>
+ </tal:block>
+ <tal:block tal:condition="attributes">
+ <tal:block tal:repeat="attr attributes">
+ <input type="checkbox" class="filter" checked="checked"
+ onchange="PyAMS_zodbbrowser.filterHistory()"
+ tal:attributes="name string:${attr.name}"/>
+ <span class="attr">
+ <strong tal:content="attr.name" />:
+ <tal:block replace="structure attr.rendered_value()" />
+ </span>
+ <br />
+ </tal:block>
+ <div class="buttons">
+ <a class="jsbutton" href="javascript:PyAMS_zodbbrowser.filterAll()"
+ >show all</a>
+ <a class="jsbutton" href="javascript:PyAMS_zodbbrowser.filterNone()"
+ >hide all</a>
+ </div>
+ </tal:block>
+ </div>
+ </div>
+
+ <div class="items"
+ tal:define="items view.listItems()"
+ tal:condition="python:items is not None">
+ <h3 class="expander">
+ <img src="/--static--/pyams_zodbbrowser/img/collapse.png"
+ alt="collapse" /> Items (<span tal:replace="python: len(items)"></span>)
+ </h3>
+
+ <div class="collapsible">
+ <tal:block tal:condition="not:items">
+ <span class="empty">There are none.</span>
+ </tal:block>
+ <tal:block tal:repeat="item items">
+ <strong tal:content="item.name" />:
+ <tal:block replace="structure item.rendered_value()" />
+ <br />
+ </tal:block>
+ </div>
+ </div>
+
+ <div class="history"
+ tal:define="history view.listHistory()"
+ tal:condition="history">
+ <h3 class="expander">
+ <img src="/--static--/pyams_zodbbrowser/img/collapse.png"
+ alt="collapse" /> History
+ </h3>
+
+ <div class="collapsible">
+ <div tal:attributes="class python:view.getRequestedTid() and 'none' or 'latest'">
+ <h4 class="transaction">
+ <a class="title" tal:attributes="href string:#zodbbrowser?oid=${view.getObjectIdHex()}"
+ >Latest</a>
+ </h4>
+ </div>
+ <div class="transaction" tal:repeat="history history"
+ tal:attributes="class python:(history['current'] or repeat['history'].start() and not view.getRequestedTid())
+ and 'transaction current' or 'transaction'">
+ <h4 class="transaction" tal:attributes="id string:tid${history.utid}">
+ <a class="subtitle"
+ tal:attributes="href string:#zodbbrowser_history?tid=${history.utid}">view transaction
+ record</a>
+ <a class="title" tal:attributes="href history.href">
+ #<span tal:replace="history.index" />:
+ <span class="timestamp" tal:content="string:${history.utc_timestamp}" title="UTC" />
+ <span class="user" tal:content="history.user_id"
+ tal:attributes="title string:user from site ${history.user_location}" />
+ <span class="location" tal:content="history.location|nothing"
+ tal:attributes="title string:request type ${history.request_type|nothing}" />
+ <span class="description" tal:content="history.description" />
+ </a>
+ </h4>
+
+ <div class="toolbox" tal:condition="python: not repeat['history'].start() and not view.readonly">
+ <form action="zodbbrowser" class="rollback" method="post" data-async>
+ <input type="hidden" name="oid" tal:attributes="value view.getObjectId()" />
+ <input type="hidden" name="tid" tal:attributes="value view.getRequestedTid()" />
+ <input type="hidden" name="rtid" tal:attributes="value history.utid" />
+ <input type="hidden" name="confirmed" value="0" />
+ <input type="hidden" name="ROLLBACK" value="" />
+ <input type="submit" class="rollbackbtn" value="Revert to this state" />
+ </form>
+ </div>
+ <div class="error" tal:condition="history.error">
+ Could not load historical state:
+ <span tal:replace="history.error" />
+ </div>
+ <div class="diff" tal:replace="structure history.diff">
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+<img id="collapseImg" style="display:none" alt=""
+ src="/--static--/pyams_zodbbrowser/img/collapse.png" />
+<img id="expandImg" style="display:none" alt=""
+ src="/--static--/pyams_zodbbrowser/img/expand.png" />
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zodbbrowser/zmi/views.py Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,568 @@
+#
+# 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.
+#
+from pyramid.httpexceptions import HTTPFound
+from pyramid.view import view_config
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+import json
+import time
+import transaction
+from html import escape
+
+# import interfaces
+from pyams_skin.interfaces import IInnerPage
+from pyams_skin.layer import IPyAMSLayer
+from pyams_zmi.interfaces.menu import IControlPanelMenu
+from pyams_zmi.layer import IAdminLayer
+from pyams_zodbbrowser.interfaces import IValueRenderer, IDatabaseHistory
+
+# import packages
+from BTrees.utils import oid_repr
+from persistent import Persistent
+from persistent.timestamp import TimeStamp
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.viewlet.menu import MenuItem
+from pyams_template.template import template_config
+from pyams_utils.adapter import ContextRequestAdapter
+from pyams_utils.interfaces import PYAMS_APPLICATION_DEFAULT_NAME, PYAMS_APPLICATION_SETTINGS_KEY
+from pyams_utils.property import cached_property
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zodbbrowser.diff import compareDictsHTML
+from pyams_zodbbrowser.history import ZodbObjectHistory
+from pyams_zodbbrowser.state import ZodbObjectState
+from pyams_zodbbrowser.value import TRUNCATIONS, pruneTruncations
+from ZODB.POSException import POSKeyError
+from ZODB.utils import p64, u64, tid_repr
+from zope.exceptions.interfaces import UserError
+from zope.interface import implementer, Interface
+
+from pyams_zodbbrowser import _
+
+
+def getObjectType(obj):
+ cls = getattr(obj, '__class__', None)
+ if type(obj) is not cls:
+ return '%s - %s' % (type(obj), cls)
+ else:
+ return str(cls)
+
+
+def getObjectTypeShort(obj):
+ cls = getattr(obj, '__class__', None)
+ if type(obj) is not cls:
+ return '%s - %s' % (type(obj).__name__, cls.__name__)
+ else:
+ return cls.__name__
+
+
+def getObjectPath(obj, tid):
+ path = []
+ seen_root = False
+ state = ZodbObjectState(obj, tid)
+ while True:
+ if state.isRoot():
+ path.append('/')
+ seen_root = True
+ else:
+ if path:
+ path.append('/')
+ if not state.getName() and state.getParentState() is None:
+ # not using hex() because we don't want L suffixes for
+ # 64-bit values
+ path.append('0x%x' % state.getObjectId())
+ break
+ path.append(state.getName() or '???')
+ state = state.getParentState()
+ if state is None:
+ if not seen_root:
+ path.append('/')
+ path.append('...')
+ path.append('/')
+ break
+ return ''.join(path[::-1])
+
+
+class ZodbObjectAttribute(object):
+
+ def __init__(self, name, value, tid=None):
+ self.name = name
+ self.value = value
+ self.tid = tid
+
+ def rendered_name(self):
+ return IValueRenderer(self.name).render(self.tid)
+
+ def rendered_value(self):
+ return IValueRenderer(self.value).render(self.tid)
+
+ def __repr__(self):
+ return '%s(%r, %r, %r)' % (self.__class__.__name__, self.name,
+ self.value, self.tid)
+
+ def __eq__(self, other):
+ if self.__class__ != other.__class__:
+ return False
+ return (self.name, self.value, self.tid) == (other.name, other.value,
+ other.tid)
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+
+@viewlet_config(name='zodbbrowser.menu', layer=IAdminLayer, context=Interface, manager=IControlPanelMenu,
+ permission='system.manage', weight=9999)
+class ZODBBrowserMenu(MenuItem):
+ """ZODB browser menu"""
+
+ label = _("ZODB browser")
+ icon_class = 'fa fa-fw fa-database'
+ url = '#zodbbrowser'
+
+
+class VeryCarefulView(ContextRequestAdapter):
+ """Base ZODB view"""
+
+ made_changes = False
+
+ @cached_property
+ def jar(self):
+ try:
+ return self.request.annotations['ZODB.interfaces.IConnection']
+ except (KeyError, AttributeError):
+ obj = self.findClosestPersistent()
+ if obj is None:
+ raise Exception("ZODB connection not available for this request")
+ return obj._p_jar
+
+ @property
+ def readonly(self):
+ return self.jar.isReadOnly()
+
+ def findClosestPersistent(self):
+ obj = self.context
+ while not isinstance(obj, Persistent):
+ try:
+ obj = obj.__parent__
+ except AttributeError:
+ return None
+ return obj
+
+
+@pagelet_config(name='zodbbrowser', context=Interface, layer=IPyAMSLayer, permission='system.manage')
+@template_config(template='templates/zodbinfo.pt', layer=IPyAMSLayer)
+@implementer(IInnerPage)
+class ZodbInfoView(VeryCarefulView):
+ """ZODB info view"""
+
+ def update(self):
+ super(ZodbInfoView, self).update()
+ pruneTruncations()
+ self.obj = self.selectObjectToView()
+ # Not using IObjectHistory(self.obj) because LP#1185175
+ self.history = ZodbObjectHistory(self.obj)
+ self.latest = True
+ if self.request.params.get('tid'):
+ self.state = ZodbObjectState(self.obj,
+ p64(int(self.request.params['tid'], 0)),
+ _history=self.history)
+ self.latest = False
+ else:
+ self.state = ZodbObjectState(self.obj, _history=self.history)
+
+ if 'CANCEL' in self.request.params:
+ raise self._redirectToSelf()
+
+ if 'ROLLBACK' in self.request.params:
+ rtid = p64(int(self.request.params['rtid'], 0))
+ self.requestedState = self._tidToTimestamp(rtid)
+ if self.request.params.get('confirmed') == '1':
+ self.history.rollback(rtid)
+ transaction.get().note('Rollback to old state %s'
+ % self.requestedState)
+ self.made_changes = True
+ transaction.get().commit()
+ raise self._redirectToSelf()
+
+ def _redirectToSelf(self):
+ return HTTPFound(self.getUrl())
+
+ def selectObjectToView(self):
+ params = self.request.params
+ obj = None
+ if 'oid' not in params:
+ obj = self.findClosestPersistent()
+ # Sanity check: if we're running in standalone mode,
+ # self.context is a Folder in the just-created MappingStorage,
+ # which we're not interested in.
+ if obj is not None and obj._p_jar is not self.jar:
+ obj = None
+ if obj is None:
+ if 'oid' in params:
+ try:
+ oid = int(params['oid'], 0)
+ except ValueError:
+ raise UserError('OID is not an integer: %r' % params['oid'])
+ else:
+ oid = self.getRootOid()
+ try:
+ obj = self.jar.get(p64(oid))
+ except POSKeyError:
+ raise UserError('There is no object with OID 0x%x' % oid)
+ return obj
+
+ def getRequestedTid(self):
+ if 'tid' in self.request.params:
+ return self.request.params['tid']
+ else:
+ return None
+
+ def getRequestedTidNice(self):
+ if 'tid' in self.request.params:
+ return self._tidToTimestamp(p64(int(self.request.params['tid'], 0)))
+ else:
+ return None
+
+ def getObjectId(self):
+ return self.state.getObjectId()
+
+ def getObjectIdHex(self):
+ return '0x%x' % self.state.getObjectId()
+
+ def getObjectType(self):
+ return getObjectType(self.obj)
+
+ def getObjectTypeShort(self):
+ return getObjectTypeShort(self.obj)
+
+ def getStateTid(self):
+ return u64(self.state.tid)
+
+ def getStateTidNice(self):
+ return self._tidToTimestamp(self.state.tid)
+
+ def getPickleSize(self):
+ return len(self.state.pickledState)
+
+ def getRootOid(self):
+ root = self.jar.root()
+ try:
+ settings = self.request.registry.settings
+ root = root[settings.get(PYAMS_APPLICATION_SETTINGS_KEY, PYAMS_APPLICATION_DEFAULT_NAME)]
+ except KeyError:
+ pass
+ return u64(root._p_oid)
+
+ def locate(self, path):
+ not_found = object() # marker
+
+ # our current position
+ # partial -- path of the last _persistent_ object
+ # here -- path of the last object traversed
+ # oid -- oid of the last _persistent_ object
+ # obj -- last object traversed
+ partial = here = '/'
+ oid = self.getRootOid()
+ obj = self.jar.get(p64(oid))
+
+ steps = path.split('/')
+
+ if steps and steps[0]:
+ # 0x1234/sub/path -> start traversal at oid 0x1234
+ try:
+ oid = int(steps[0], 0)
+ except ValueError:
+ pass
+ else:
+ partial = here = hex(oid)
+ try:
+ obj = self.jar.get(p64(oid))
+ except KeyError:
+ oid = self.getRootOid()
+ return dict(error='Not found: %s' % steps[0],
+ partial_oid=oid,
+ partial_path='/',
+ partial_url=self.getUrl(oid))
+ steps = steps[1:]
+
+ for step in steps:
+ if not step:
+ continue
+ if not here.endswith('/'):
+ here += '/'
+ here += step.encode('utf-8')
+ try:
+ child = obj[step]
+ except Exception:
+ child = getattr(obj, step, not_found)
+ if child is not_found:
+ return dict(error='Not found: %s' % here,
+ partial_oid=oid,
+ partial_path=partial,
+ partial_url=self.getUrl(oid))
+ obj = child
+ if isinstance(obj, Persistent):
+ partial = here
+ oid = u64(obj._p_oid)
+ if not isinstance(obj, Persistent):
+ return dict(error='Not persistent: %s' % here,
+ partial_oid=oid,
+ partial_path=partial,
+ partial_url=self.getUrl(oid))
+ return dict(oid=oid,
+ url=self.getUrl(oid))
+
+ def getUrl(self, oid=None, tid=None):
+ if oid is None:
+ oid = self.getObjectId()
+ url = "#zodbbrowser?oid=0x%x" % oid
+ if tid is None and 'tid' in self.request.params:
+ url += "&tid=" + self.request.params['tid']
+ elif tid is not None:
+ url += "&tid=0x%x" % tid
+ return url
+
+ def getBreadcrumbs(self):
+ breadcrumbs = []
+ state = self.state
+ seen_root = False
+ while True:
+ url = self.getUrl(state.getObjectId())
+ if state.isRoot():
+ breadcrumbs.append(('/', url))
+ seen_root = True
+ else:
+ if breadcrumbs:
+ breadcrumbs.append(('/', None))
+ if not state.getName() and state.getParentState() is None:
+ # not using hex() because we don't want L suffixes for
+ # 64-bit values
+ breadcrumbs.append(('0x%x' % state.getObjectId(), url))
+ break
+ breadcrumbs.append((state.getName() or '???', url))
+ state = state.getParentState()
+ if state is None:
+ if not seen_root:
+ url = self.getUrl(self.getRootOid())
+ breadcrumbs.append(('/', None))
+ breadcrumbs.append(('...', None))
+ breadcrumbs.append(('/', url))
+ break
+ return breadcrumbs[::-1]
+
+ def getPath(self):
+ return ''.join(name for name, url in self.getBreadcrumbs())
+
+ def getBreadcrumbsHTML(self):
+ html = []
+ for name, url in self.getBreadcrumbs():
+ if url:
+ html.append('<a href="%s">%s</a>' % (escape(url, True),
+ escape(name)))
+ else:
+ html.append(escape(name))
+ return ''.join(html)
+
+ def listAttributes(self):
+ attrs = self.state.listAttributes()
+ if attrs is None:
+ return None
+ return [ZodbObjectAttribute(name, value, self.state.requestedTid)
+ for name, value in sorted(attrs)]
+
+ def listItems(self):
+ items = self.state.listItems()
+ if items is None:
+ return None
+ return [ZodbObjectAttribute(name, value, self.state.requestedTid)
+ for name, value in items]
+
+ def _loadHistoricalState(self):
+ results = []
+ for d in self.history:
+ try:
+ interp = ZodbObjectState(self.obj, d['tid'],
+ _history=self.history)
+ state = interp.asDict()
+ error = interp.getError()
+ except Exception as e:
+ state = {}
+ error = '%s: %s' % (e.__class__.__name__, e)
+ results.append(dict(state=state, error=error))
+ results.append(dict(state={}, error=None))
+ return results
+
+ def listHistory(self):
+ """List transactions that modified a persistent object."""
+ state = self._loadHistoricalState()
+ results = []
+ for n, d in enumerate(self.history):
+ utc_timestamp = str(time.strftime('%Y-%m-%d %H:%M:%S',
+ time.gmtime(d['time'])))
+ local_timestamp = str(time.strftime('%Y-%m-%d %H:%M:%S',
+ time.localtime(d['time'])))
+ try:
+ user_location, user_id = d['user_name'].split()
+ except ValueError:
+ user_location = None
+ user_id = d['user_name']
+ url = self.getUrl(tid=u64(d['tid']))
+ current = (d['tid'] == self.state.tid and
+ self.state.requestedTid is not None)
+ curState = state[n]['state']
+ oldState = state[n + 1]['state']
+ diff = compareDictsHTML(curState, oldState, d['tid'])
+
+ results.append(dict(utid=u64(d['tid']),
+ href=url, current=current,
+ error=state[n]['error'],
+ diff=diff, user_id=user_id,
+ user_location=user_location,
+ utc_timestamp=utc_timestamp,
+ local_timestamp=local_timestamp, **d))
+
+ # number in reverse order
+ for i in range(len(results)):
+ results[i]['index'] = len(results) - i
+
+ return results
+
+ def _tidToTimestamp(self, tid):
+ if isinstance(tid, str) and len(tid) == 8:
+ return str(TimeStamp(tid))
+ return tid_repr(tid)
+
+
+@view_config(name='zodbbrowser_path_to_oid', context=Interface, request_type=IPyAMSLayer,
+ permission='system.manage', renderer='json', xhr=True)
+class PathToOidView(ZodbInfoView):
+
+ def __call__(self):
+ path = self.request.params.get('path')
+ return self.locate(path)
+
+
+@view_config(name='zodbbrowser_truncated', context=Interface, request_type=IPyAMSLayer,
+ permission='system.manage', renderer='json', xhr=True)
+class TruncatedView(ZodbInfoView):
+
+ def __call__(self):
+ id = self.request.params.get('id')
+ return TRUNCATIONS.get(id)
+
+
+@pagelet_config(name='zodbbrowser_history', context=Interface, layer=IPyAMSLayer, permission='system.manage')
+@template_config(template='templates/zodbhistory.pt', layer=IPyAMSLayer)
+@implementer(IInnerPage)
+class ZodbHistoryView(VeryCarefulView):
+ """Zodb history view"""
+
+ page_size = 5
+
+ def update(self):
+ super(ZodbHistoryView, self).update()
+ pruneTruncations()
+ params = self.request.params
+ if 'page_size' in params:
+ self.page_size = max(1, int(params['page_size']))
+ self.history = IDatabaseHistory(self.jar)
+ if 'page' in params:
+ self.page = int(params['page'])
+ elif 'tid' in params:
+ tid = int(params['tid'], 0)
+ self.page = self.findPage(p64(tid))
+ else:
+ self.page = 0
+ self.last_page = max(0, len(self.history) - 1) // self.page_size
+ if self.page > self.last_page:
+ self.page = self.last_page
+ self.last_idx = max(0, len(self.history) - self.page * self.page_size)
+ self.first_idx = max(0, self.last_idx - self.page_size)
+
+ def getUrl(self, tid=None):
+ url = "#zodbbrowser_history"
+ if tid is None and 'tid' in self.request.params:
+ url += "?tid=" + self.request.params['tid']
+ elif tid is not None:
+ url += "?tid=0x%x" % tid
+ return url
+
+ def findPage(self, tid):
+ try:
+ pos = list(self.history.tids).index(tid)
+ except ValueError:
+ return 0
+ else:
+ return (len(self.history) - pos - 1) // self.page_size
+
+ def listHistory(self):
+ if 'tid' in self.request.params:
+ requested_tid = p64(int(self.request.params['tid'], 0))
+ else:
+ requested_tid = None
+
+ results = []
+ for n, d in enumerate(self.history[self.first_idx:self.last_idx]):
+ utid = u64(d.tid)
+ ts = TimeStamp(d.tid).timeTime()
+ utc_timestamp = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(ts))
+ local_timestamp = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(ts))
+ try:
+ user_location, user_id = d.user.split()
+ except ValueError:
+ user_location = None
+ user_id = d.user
+ try:
+ size = d._tend - d._tpos
+ except AttributeError:
+ size = None
+ ext = d.extension if isinstance(d.extension, dict) else {}
+ objects = []
+ for record in d:
+ obj = self.jar.get(record.oid)
+ url = "#zodbbrowser?oid=0x%x&tid=0x%x" % (u64(record.oid),
+ utid)
+ objects.append(dict(
+ oid=u64(record.oid),
+ path=getObjectPath(obj, d.tid),
+ oid_repr=oid_repr(record.oid),
+ class_repr=getObjectType(obj),
+ url=url,
+ repr=IValueRenderer(obj).render(d.tid),
+ ))
+ if len(objects) == 1:
+ summary = '1 object record'
+ else:
+ summary = '%d object records' % len(objects)
+ if size is not None:
+ summary += ' (%d bytes)' % size
+ results.append(dict(
+ index=(self.first_idx + n + 1),
+ utc_timestamp=utc_timestamp,
+ local_timestamp=local_timestamp,
+ user_id=user_id,
+ user_location=user_location,
+ description=d.description,
+ utid=utid,
+ current=(d.tid == requested_tid),
+ href=self.getUrl(tid=utid),
+ size=size,
+ summary=summary,
+ hidden=(len(objects) > 5),
+ objects=objects,
+ **ext
+ ))
+ if results and not requested_tid and self.page == 0:
+ results[-1]['current'] = True
+ return results[::-1]