src/pyams_zodbbrowser/zmi/views.py
changeset 0 a02202f95e2c
child 4 f2a4feee1ff3
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_zodbbrowser/zmi/views.py	Wed Mar 11 12:27:00 2015 +0100
@@ -0,0 +1,568 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+from pyramid.httpexceptions import HTTPFound
+from pyramid.view import view_config
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+import json
+import time
+import transaction
+from html import escape
+
+# import interfaces
+from pyams_skin.interfaces import IInnerPage
+from pyams_skin.layer import IPyAMSLayer
+from pyams_zmi.interfaces.menu import IControlPanelMenu
+from pyams_zmi.layer import IAdminLayer
+from pyams_zodbbrowser.interfaces import IValueRenderer, IDatabaseHistory
+
+# import packages
+from BTrees.utils import oid_repr
+from persistent import Persistent
+from persistent.timestamp import TimeStamp
+from pyams_pagelet.pagelet import pagelet_config
+from pyams_skin.viewlet.menu import MenuItem
+from pyams_template.template import template_config
+from pyams_utils.adapter import ContextRequestAdapter
+from pyams_utils.interfaces import PYAMS_APPLICATION_DEFAULT_NAME, PYAMS_APPLICATION_SETTINGS_KEY
+from pyams_utils.property import cached_property
+from pyams_viewlet.viewlet import viewlet_config
+from pyams_zodbbrowser.diff import compareDictsHTML
+from pyams_zodbbrowser.history import ZodbObjectHistory
+from pyams_zodbbrowser.state import ZodbObjectState
+from pyams_zodbbrowser.value import TRUNCATIONS, pruneTruncations
+from ZODB.POSException import POSKeyError
+from ZODB.utils import p64, u64, tid_repr
+from zope.exceptions.interfaces import UserError
+from zope.interface import implementer, Interface
+
+from pyams_zodbbrowser import _
+
+
+def getObjectType(obj):
+    cls = getattr(obj, '__class__', None)
+    if type(obj) is not cls:
+        return '%s - %s' % (type(obj), cls)
+    else:
+        return str(cls)
+
+
+def getObjectTypeShort(obj):
+    cls = getattr(obj, '__class__', None)
+    if type(obj) is not cls:
+        return '%s - %s' % (type(obj).__name__, cls.__name__)
+    else:
+        return cls.__name__
+
+
+def getObjectPath(obj, tid):
+    path = []
+    seen_root = False
+    state = ZodbObjectState(obj, tid)
+    while True:
+        if state.isRoot():
+            path.append('/')
+            seen_root = True
+        else:
+            if path:
+                path.append('/')
+            if not state.getName() and state.getParentState() is None:
+                # not using hex() because we don't want L suffixes for
+                # 64-bit values
+                path.append('0x%x' % state.getObjectId())
+                break
+            path.append(state.getName() or '???')
+        state = state.getParentState()
+        if state is None:
+            if not seen_root:
+                path.append('/')
+                path.append('...')
+                path.append('/')
+            break
+    return ''.join(path[::-1])
+
+
+class ZodbObjectAttribute(object):
+
+    def __init__(self, name, value, tid=None):
+        self.name = name
+        self.value = value
+        self.tid = tid
+
+    def rendered_name(self):
+        return IValueRenderer(self.name).render(self.tid)
+
+    def rendered_value(self):
+        return IValueRenderer(self.value).render(self.tid)
+
+    def __repr__(self):
+        return '%s(%r, %r, %r)' % (self.__class__.__name__, self.name,
+                                   self.value, self.tid)
+
+    def __eq__(self, other):
+        if self.__class__ != other.__class__:
+            return False
+        return (self.name, self.value, self.tid) == (other.name, other.value,
+                                                     other.tid)
+
+    def __ne__(self, other):
+        return not self.__eq__(other)
+
+
+@viewlet_config(name='zodbbrowser.menu', layer=IAdminLayer, context=Interface, manager=IControlPanelMenu,
+                permission='system.manage', weight=9999)
+class ZODBBrowserMenu(MenuItem):
+    """ZODB browser menu"""
+
+    label = _("ZODB browser")
+    icon_class = 'fa fa-fw fa-database'
+    url = '#zodbbrowser'
+
+
+class VeryCarefulView(ContextRequestAdapter):
+    """Base ZODB view"""
+
+    made_changes = False
+
+    @cached_property
+    def jar(self):
+        try:
+            return self.request.annotations['ZODB.interfaces.IConnection']
+        except (KeyError, AttributeError):
+            obj = self.findClosestPersistent()
+            if obj is None:
+                raise Exception("ZODB connection not available for this request")
+            return obj._p_jar
+
+    @property
+    def readonly(self):
+        return self.jar.isReadOnly()
+
+    def findClosestPersistent(self):
+        obj = self.context
+        while not isinstance(obj, Persistent):
+            try:
+                obj = obj.__parent__
+            except AttributeError:
+                return None
+        return obj
+
+
+@pagelet_config(name='zodbbrowser', context=Interface, layer=IPyAMSLayer, permission='system.manage')
+@template_config(template='templates/zodbinfo.pt', layer=IPyAMSLayer)
+@implementer(IInnerPage)
+class ZodbInfoView(VeryCarefulView):
+    """ZODB info view"""
+
+    def update(self):
+        super(ZodbInfoView, self).update()
+        pruneTruncations()
+        self.obj = self.selectObjectToView()
+        # Not using IObjectHistory(self.obj) because LP#1185175
+        self.history = ZodbObjectHistory(self.obj)
+        self.latest = True
+        if self.request.params.get('tid'):
+            self.state = ZodbObjectState(self.obj,
+                                         p64(int(self.request.params['tid'], 0)),
+                                         _history=self.history)
+            self.latest = False
+        else:
+            self.state = ZodbObjectState(self.obj, _history=self.history)
+
+        if 'CANCEL' in self.request.params:
+            raise self._redirectToSelf()
+
+        if 'ROLLBACK' in self.request.params:
+            rtid = p64(int(self.request.params['rtid'], 0))
+            self.requestedState = self._tidToTimestamp(rtid)
+            if self.request.params.get('confirmed') == '1':
+                self.history.rollback(rtid)
+                transaction.get().note('Rollback to old state %s'
+                                       % self.requestedState)
+                self.made_changes = True
+                transaction.get().commit()
+                raise self._redirectToSelf()
+
+    def _redirectToSelf(self):
+        return HTTPFound(self.getUrl())
+
+    def selectObjectToView(self):
+        params = self.request.params
+        obj = None
+        if 'oid' not in params:
+            obj = self.findClosestPersistent()
+            # Sanity check: if we're running in standalone mode,
+            # self.context is a Folder in the just-created MappingStorage,
+            # which we're not interested in.
+            if obj is not None and obj._p_jar is not self.jar:
+                obj = None
+        if obj is None:
+            if 'oid' in params:
+                try:
+                    oid = int(params['oid'], 0)
+                except ValueError:
+                    raise UserError('OID is not an integer: %r' % params['oid'])
+            else:
+                oid = self.getRootOid()
+            try:
+                obj = self.jar.get(p64(oid))
+            except POSKeyError:
+                raise UserError('There is no object with OID 0x%x' % oid)
+        return obj
+
+    def getRequestedTid(self):
+        if 'tid' in self.request.params:
+            return self.request.params['tid']
+        else:
+            return None
+
+    def getRequestedTidNice(self):
+        if 'tid' in self.request.params:
+            return self._tidToTimestamp(p64(int(self.request.params['tid'], 0)))
+        else:
+            return None
+
+    def getObjectId(self):
+        return self.state.getObjectId()
+
+    def getObjectIdHex(self):
+        return '0x%x' % self.state.getObjectId()
+
+    def getObjectType(self):
+        return getObjectType(self.obj)
+
+    def getObjectTypeShort(self):
+        return getObjectTypeShort(self.obj)
+
+    def getStateTid(self):
+        return u64(self.state.tid)
+
+    def getStateTidNice(self):
+        return self._tidToTimestamp(self.state.tid)
+
+    def getPickleSize(self):
+        return len(self.state.pickledState)
+
+    def getRootOid(self):
+        root = self.jar.root()
+        try:
+            settings = self.request.registry.settings
+            root = root[settings.get(PYAMS_APPLICATION_SETTINGS_KEY, PYAMS_APPLICATION_DEFAULT_NAME)]
+        except KeyError:
+            pass
+        return u64(root._p_oid)
+
+    def locate(self, path):
+        not_found = object()  # marker
+
+        # our current position
+        #   partial -- path of the last _persistent_ object
+        #   here -- path of the last object traversed
+        #   oid -- oid of the last _persistent_ object
+        #   obj -- last object traversed
+        partial = here = '/'
+        oid = self.getRootOid()
+        obj = self.jar.get(p64(oid))
+
+        steps = path.split('/')
+
+        if steps and steps[0]:
+            # 0x1234/sub/path -> start traversal at oid 0x1234
+            try:
+                oid = int(steps[0], 0)
+            except ValueError:
+                pass
+            else:
+                partial = here = hex(oid)
+                try:
+                    obj = self.jar.get(p64(oid))
+                except KeyError:
+                    oid = self.getRootOid()
+                    return dict(error='Not found: %s' % steps[0],
+                                partial_oid=oid,
+                                partial_path='/',
+                                partial_url=self.getUrl(oid))
+                steps = steps[1:]
+
+        for step in steps:
+            if not step:
+                continue
+            if not here.endswith('/'):
+                here += '/'
+            here += step.encode('utf-8')
+            try:
+                child = obj[step]
+            except Exception:
+                child = getattr(obj, step, not_found)
+                if child is not_found:
+                    return dict(error='Not found: %s' % here,
+                                partial_oid=oid,
+                                partial_path=partial,
+                                partial_url=self.getUrl(oid))
+            obj = child
+            if isinstance(obj, Persistent):
+                partial = here
+                oid = u64(obj._p_oid)
+        if not isinstance(obj, Persistent):
+            return dict(error='Not persistent: %s' % here,
+                        partial_oid=oid,
+                        partial_path=partial,
+                        partial_url=self.getUrl(oid))
+        return dict(oid=oid,
+                    url=self.getUrl(oid))
+
+    def getUrl(self, oid=None, tid=None):
+        if oid is None:
+            oid = self.getObjectId()
+        url = "#zodbbrowser?oid=0x%x" % oid
+        if tid is None and 'tid' in self.request.params:
+            url += "&tid=" + self.request.params['tid']
+        elif tid is not None:
+            url += "&tid=0x%x" % tid
+        return url
+
+    def getBreadcrumbs(self):
+        breadcrumbs = []
+        state = self.state
+        seen_root = False
+        while True:
+            url = self.getUrl(state.getObjectId())
+            if state.isRoot():
+                breadcrumbs.append(('/', url))
+                seen_root = True
+            else:
+                if breadcrumbs:
+                    breadcrumbs.append(('/', None))
+                if not state.getName() and state.getParentState() is None:
+                    # not using hex() because we don't want L suffixes for
+                    # 64-bit values
+                    breadcrumbs.append(('0x%x' % state.getObjectId(), url))
+                    break
+                breadcrumbs.append((state.getName() or '???', url))
+            state = state.getParentState()
+            if state is None:
+                if not seen_root:
+                    url = self.getUrl(self.getRootOid())
+                    breadcrumbs.append(('/', None))
+                    breadcrumbs.append(('...', None))
+                    breadcrumbs.append(('/', url))
+                break
+        return breadcrumbs[::-1]
+
+    def getPath(self):
+        return ''.join(name for name, url in self.getBreadcrumbs())
+
+    def getBreadcrumbsHTML(self):
+        html = []
+        for name, url in self.getBreadcrumbs():
+            if url:
+                html.append('<a href="%s">%s</a>' % (escape(url, True),
+                                                     escape(name)))
+            else:
+                html.append(escape(name))
+        return ''.join(html)
+
+    def listAttributes(self):
+        attrs = self.state.listAttributes()
+        if attrs is None:
+            return None
+        return [ZodbObjectAttribute(name, value, self.state.requestedTid)
+                for name, value in sorted(attrs)]
+
+    def listItems(self):
+        items = self.state.listItems()
+        if items is None:
+            return None
+        return [ZodbObjectAttribute(name, value, self.state.requestedTid)
+                for name, value in items]
+
+    def _loadHistoricalState(self):
+        results = []
+        for d in self.history:
+            try:
+                interp = ZodbObjectState(self.obj, d['tid'],
+                                         _history=self.history)
+                state = interp.asDict()
+                error = interp.getError()
+            except Exception as e:
+                state = {}
+                error = '%s: %s' % (e.__class__.__name__, e)
+            results.append(dict(state=state, error=error))
+        results.append(dict(state={}, error=None))
+        return results
+
+    def listHistory(self):
+        """List transactions that modified a persistent object."""
+        state = self._loadHistoricalState()
+        results = []
+        for n, d in enumerate(self.history):
+            utc_timestamp = str(time.strftime('%Y-%m-%d %H:%M:%S',
+                                              time.gmtime(d['time'])))
+            local_timestamp = str(time.strftime('%Y-%m-%d %H:%M:%S',
+                                                time.localtime(d['time'])))
+            try:
+                user_location, user_id = d['user_name'].split()
+            except ValueError:
+                user_location = None
+                user_id = d['user_name']
+            url = self.getUrl(tid=u64(d['tid']))
+            current = (d['tid'] == self.state.tid and
+                       self.state.requestedTid is not None)
+            curState = state[n]['state']
+            oldState = state[n + 1]['state']
+            diff = compareDictsHTML(curState, oldState, d['tid'])
+
+            results.append(dict(utid=u64(d['tid']),
+                                href=url, current=current,
+                                error=state[n]['error'],
+                                diff=diff, user_id=user_id,
+                                user_location=user_location,
+                                utc_timestamp=utc_timestamp,
+                                local_timestamp=local_timestamp, **d))
+
+        # number in reverse order
+        for i in range(len(results)):
+            results[i]['index'] = len(results) - i
+
+        return results
+
+    def _tidToTimestamp(self, tid):
+        if isinstance(tid, str) and len(tid) == 8:
+            return str(TimeStamp(tid))
+        return tid_repr(tid)
+
+
+@view_config(name='zodbbrowser_path_to_oid', context=Interface, request_type=IPyAMSLayer,
+             permission='system.manage', renderer='json', xhr=True)
+class PathToOidView(ZodbInfoView):
+
+    def __call__(self):
+        path = self.request.params.get('path')
+        return self.locate(path)
+
+
+@view_config(name='zodbbrowser_truncated', context=Interface, request_type=IPyAMSLayer,
+             permission='system.manage', renderer='json', xhr=True)
+class TruncatedView(ZodbInfoView):
+
+    def __call__(self):
+        id = self.request.params.get('id')
+        return TRUNCATIONS.get(id)
+
+
+@pagelet_config(name='zodbbrowser_history', context=Interface, layer=IPyAMSLayer, permission='system.manage')
+@template_config(template='templates/zodbhistory.pt', layer=IPyAMSLayer)
+@implementer(IInnerPage)
+class ZodbHistoryView(VeryCarefulView):
+    """Zodb history view"""
+
+    page_size = 5
+
+    def update(self):
+        super(ZodbHistoryView, self).update()
+        pruneTruncations()
+        params = self.request.params
+        if 'page_size' in params:
+            self.page_size = max(1, int(params['page_size']))
+        self.history = IDatabaseHistory(self.jar)
+        if 'page' in params:
+            self.page = int(params['page'])
+        elif 'tid' in params:
+            tid = int(params['tid'], 0)
+            self.page = self.findPage(p64(tid))
+        else:
+            self.page = 0
+        self.last_page = max(0, len(self.history) - 1) // self.page_size
+        if self.page > self.last_page:
+            self.page = self.last_page
+        self.last_idx = max(0, len(self.history) - self.page * self.page_size)
+        self.first_idx = max(0, self.last_idx - self.page_size)
+
+    def getUrl(self, tid=None):
+        url = "#zodbbrowser_history"
+        if tid is None and 'tid' in self.request.params:
+            url += "?tid=" + self.request.params['tid']
+        elif tid is not None:
+            url += "?tid=0x%x" % tid
+        return url
+
+    def findPage(self, tid):
+        try:
+            pos = list(self.history.tids).index(tid)
+        except ValueError:
+            return 0
+        else:
+            return (len(self.history) - pos - 1) // self.page_size
+
+    def listHistory(self):
+        if 'tid' in self.request.params:
+            requested_tid = p64(int(self.request.params['tid'], 0))
+        else:
+            requested_tid = None
+
+        results = []
+        for n, d in enumerate(self.history[self.first_idx:self.last_idx]):
+            utid = u64(d.tid)
+            ts = TimeStamp(d.tid).timeTime()
+            utc_timestamp = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(ts))
+            local_timestamp = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(ts))
+            try:
+                user_location, user_id = d.user.split()
+            except ValueError:
+                user_location = None
+                user_id = d.user
+            try:
+                size = d._tend - d._tpos
+            except AttributeError:
+                size = None
+            ext = d.extension if isinstance(d.extension, dict) else {}
+            objects = []
+            for record in d:
+                obj = self.jar.get(record.oid)
+                url = "#zodbbrowser?oid=0x%x&tid=0x%x" % (u64(record.oid),
+                                                           utid)
+                objects.append(dict(
+                    oid=u64(record.oid),
+                    path=getObjectPath(obj, d.tid),
+                    oid_repr=oid_repr(record.oid),
+                    class_repr=getObjectType(obj),
+                    url=url,
+                    repr=IValueRenderer(obj).render(d.tid),
+                ))
+            if len(objects) == 1:
+                summary = '1 object record'
+            else:
+                summary = '%d object records' % len(objects)
+            if size is not None:
+                summary += ' (%d bytes)' % size
+            results.append(dict(
+                index=(self.first_idx + n + 1),
+                utc_timestamp=utc_timestamp,
+                local_timestamp=local_timestamp,
+                user_id=user_id,
+                user_location=user_location,
+                description=d.description,
+                utid=utid,
+                current=(d.tid == requested_tid),
+                href=self.getUrl(tid=utid),
+                size=size,
+                summary=summary,
+                hidden=(len(objects) > 5),
+                objects=objects,
+                **ext
+            ))
+        if results and not requested_tid and self.page == 0:
+            results[-1]['current'] = True
+        return results[::-1]