# HG changeset patch # User Thierry Florac # Date 1426073220 -3600 # Node ID a02202f95e2cd6190becad9af26ad22db2d65c8b First commit diff -r 000000000000 -r a02202f95e2c .hgignore --- /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$ diff -r 000000000000 -r a02202f95e2c .installed.cfg --- /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 diff -r 000000000000 -r a02202f95e2c LICENSE --- /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. diff -r 000000000000 -r a02202f95e2c MANIFEST.in --- /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 *.*~ diff -r 000000000000 -r a02202f95e2c bootstrap.py --- /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) diff -r 000000000000 -r a02202f95e2c buildout.cfg --- /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 diff -r 000000000000 -r a02202f95e2c docs/HISTORY.txt diff -r 000000000000 -r a02202f95e2c docs/README.txt diff -r 000000000000 -r a02202f95e2c setup.py --- /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 +# 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' + ] + }) diff -r 000000000000 -r a02202f95e2c src/pyams_zodbbrowser.egg-info/PKG-INFO --- /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 diff -r 000000000000 -r a02202f95e2c src/pyams_zodbbrowser.egg-info/SOURCES.txt --- /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 diff -r 000000000000 -r a02202f95e2c src/pyams_zodbbrowser.egg-info/dependency_links.txt --- /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 @@ + diff -r 000000000000 -r a02202f95e2c src/pyams_zodbbrowser.egg-info/entry_points.txt --- /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 + diff -r 000000000000 -r a02202f95e2c src/pyams_zodbbrowser.egg-info/namespace_packages.txt --- /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 @@ + diff -r 000000000000 -r a02202f95e2c src/pyams_zodbbrowser.egg-info/not-zip-safe --- /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 @@ + diff -r 000000000000 -r a02202f95e2c src/pyams_zodbbrowser.egg-info/requires.txt --- /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] diff -r 000000000000 -r a02202f95e2c src/pyams_zodbbrowser.egg-info/top_level.txt --- /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 diff -r 000000000000 -r a02202f95e2c src/pyams_zodbbrowser/__init__.py --- /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 +# 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') diff -r 000000000000 -r a02202f95e2c src/pyams_zodbbrowser/btreesupport.py --- /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 +# 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]), + * 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]), + * 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())) diff -r 000000000000 -r a02202f95e2c src/pyams_zodbbrowser/cache.py --- /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 +# 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'] diff -r 000000000000 -r a02202f95e2c src/pyams_zodbbrowser/configure.zcml --- /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 @@ + + + + + \ No newline at end of file diff -r 000000000000 -r a02202f95e2c src/pyams_zodbbrowser/diff.py --- /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 +# 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 + '
\n'] + prefix, removed, added, suffix = compareTuples(new, old) + if len(prefix) > 0: + html.append(indent + '
\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 + '
\n') + for oldval in removed: + html.append(indent + '
\n' % REMOVED) + html.append(indent + ' %s %s\n' % ( + REMOVED, IValueRenderer(oldval).render(tid))) + html.append(indent + '
\n') + for newval in added: + html.append(indent + '
\n' % ADDED) + html.append(indent + ' %s %s\n' % ( + ADDED, IValueRenderer(newval).render(tid))) + html.append(indent + '
\n') + if len(suffix) > 0: + html.append(indent + '
\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 + '
\n') + html.append(indent + '
\n') + return ''.join(html) + + +def compareDictsHTML(new, old, tid=None, indent=''): + """Compare two state dictionaries, return HTML.""" + html = [indent + '
\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 + '
\n' % escape(what)) + if isinstance(key, str) and isascii(key): + html.append(indent + ' %s: ' % escape(key)) + else: + html.append(indent + ' %s: ' % 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 + '
\n') + html.append(indent + '
\n') + return ''.join(html) diff -r 000000000000 -r a02202f95e2c src/pyams_zodbbrowser/doctests/README.txt --- /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 +========================= diff -r 000000000000 -r a02202f95e2c src/pyams_zodbbrowser/history.py --- /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 +# 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]) diff -r 000000000000 -r a02202f95e2c src/pyams_zodbbrowser/interfaces/__init__.py --- /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 +# 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. + """ diff -r 000000000000 -r a02202f95e2c src/pyams_zodbbrowser/locales/fr/LC_MESSAGES/pyams_zodbbrowser.mo Binary file src/pyams_zodbbrowser/locales/fr/LC_MESSAGES/pyams_zodbbrowser.mo has changed diff -r 000000000000 -r a02202f95e2c src/pyams_zodbbrowser/locales/fr/LC_MESSAGES/pyams_zodbbrowser.po --- /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 , 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 \n" +"Language-Team: French \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" diff -r 000000000000 -r a02202f95e2c src/pyams_zodbbrowser/locales/pyams_zodbbrowser.pot --- /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 , 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 \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 "" diff -r 000000000000 -r a02202f95e2c src/pyams_zodbbrowser/state.py --- /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 +# 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 + # 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()) + diff -r 000000000000 -r a02202f95e2c src/pyams_zodbbrowser/tests/__init__.py --- /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 @@ + diff -r 000000000000 -r a02202f95e2c src/pyams_zodbbrowser/tests/test_utilsdocs.py --- /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 +# 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') + diff -r 000000000000 -r a02202f95e2c src/pyams_zodbbrowser/tests/test_utilsdocstrings.py --- /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 +# 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') diff -r 000000000000 -r a02202f95e2c src/pyams_zodbbrowser/value.py --- /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 +# 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 '' % self.context.__class__.__name__ + except Exception: + return '' + + def render(self, tid=None, can_link=True, limit=200): + text = self._repr() + if len(text) > limit: + id = truncate(text[limit:]) + text = '%s...' % ( + 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 + '' + suffix = '' + 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 + '
'.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 = '
' # hm, maybe '\\n
'? + if sum(map(len, lines)) > limit: + head = nl.join(lines[:5]) + tail = nl.join(lines[5:]) + id = truncate(tail) + return (prefix + "'" + head + nl + + '...' % id + + "'") + else: + return (prefix + "'" + nl.join(lines) + + "'") + + +@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 '' 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 '' 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 + '' 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 '%s' % (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 + # 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 '' % ', '.join(i.__identifier__ + for i in self.context._Provides__args[1:]) + diff -r 000000000000 -r a02202f95e2c src/pyams_zodbbrowser/zmi/__init__.py --- /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 +# 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 diff -r 000000000000 -r a02202f95e2c src/pyams_zodbbrowser/zmi/resources/css/zodbbrowser.css --- /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; +} diff -r 000000000000 -r a02202f95e2c src/pyams_zodbbrowser/zmi/resources/css/zodbbrowser.min.css --- /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 diff -r 000000000000 -r a02202f95e2c src/pyams_zodbbrowser/zmi/resources/img/collapse.png Binary file src/pyams_zodbbrowser/zmi/resources/img/collapse.png has changed diff -r 000000000000 -r a02202f95e2c src/pyams_zodbbrowser/zmi/resources/img/expand.png Binary file src/pyams_zodbbrowser/zmi/resources/img/expand.png has changed diff -r 000000000000 -r a02202f95e2c src/pyams_zodbbrowser/zmi/resources/js/zodbbrowser.js --- /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('
' + hidden_text + '
'); + } + } + }, + + collapseOrExpand: function () { + // this is
+ // the following element is
+ 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
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 = ' ' + errorMessage + ''; + $('#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 ' + + '' + + 'go to ' + data.partial_path + + '' + + ' 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'); + $('
' + + '
' + + '
' + + 'This is a dangerous operation that may break data integrity.' + + ' Are you really sure you want to do this?' + + '
' + + '' + + '' + + '
' + + '
').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); diff -r 000000000000 -r a02202f95e2c src/pyams_zodbbrowser/zmi/resources/js/zodbbrowser.min.js --- /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'+l+"
")}}},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=' '+errorMessage+"";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 go to '+c.partial_path+" 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('
This is a dangerous operation that may break data integrity. Are you really sure you want to do this?
').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 diff -r 000000000000 -r a02202f95e2c src/pyams_zodbbrowser/zmi/templates/zodbhistory.pt --- /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 @@ +
+
+

+ ZODB transactions +

+ +

+ + +

+ +
+ +
+
+

+ Newer +

+
+
+

+ Latest +

+
+
+

+ + #: + + + + + +

+ +
+
+ collapseexpand N objects saved +
+ +
+
    +
  1. + /(path) + + (obj) +
  2. +
+
+
+
+
+

+ Older +

+
+
+ +
+ + diff -r 000000000000 -r a02202f95e2c src/pyams_zodbbrowser/zmi/templates/zodbinfo.pt --- /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 @@ +
+
+

+ +

+ +

+ +

+ + bytes pickled + +

+ +
+ at + +
+

+ +
+

+ collapse Attributes +

+ +
+ +
+ Could not load the object: + +
+
+ + There are none. + + + + + + : + + +
+
+ +
+
+
+ +
+

+ collapse Items () +

+ +
+ + There are none. + + + : + +
+
+
+
+ +
+

+ collapse History +

+ +
+
+

+ Latest +

+
+
+

+ view transaction + record + + #: + + + + + +

+ +
+
+ + + + + + +
+
+
+ Could not load historical state: + +
+
+
+
+
+
+
+ + diff -r 000000000000 -r a02202f95e2c src/pyams_zodbbrowser/zmi/views.py --- /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 +# 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('%s' % (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]