[Checkins] SVN: zc.objectlog/trunk/src/zc/objectlog/ Initial import. TTW log with automatic changesets.

Gary Poster gary at zope.com
Tue Aug 15 17:11:44 EDT 2006


Log message for revision 69541:
  Initial import.  TTW log with automatic changesets.
  

Changed:
  A   zc.objectlog/trunk/src/zc/objectlog/DEPENDENCIES.cfg
  A   zc.objectlog/trunk/src/zc/objectlog/__init__.py
  A   zc.objectlog/trunk/src/zc/objectlog/browser/
  A   zc.objectlog/trunk/src/zc/objectlog/browser/__init__.py
  A   zc.objectlog/trunk/src/zc/objectlog/browser/configure.zcml
  A   zc.objectlog/trunk/src/zc/objectlog/browser/default.pt
  A   zc.objectlog/trunk/src/zc/objectlog/browser/interfaces.py
  A   zc.objectlog/trunk/src/zc/objectlog/browser/log.py
  A   zc.objectlog/trunk/src/zc/objectlog/configure.zcml
  A   zc.objectlog/trunk/src/zc/objectlog/copier.py
  A   zc.objectlog/trunk/src/zc/objectlog/copier.txt
  A   zc.objectlog/trunk/src/zc/objectlog/i18n.py
  A   zc.objectlog/trunk/src/zc/objectlog/interfaces.py
  A   zc.objectlog/trunk/src/zc/objectlog/log.py
  A   zc.objectlog/trunk/src/zc/objectlog/log.txt
  A   zc.objectlog/trunk/src/zc/objectlog/tests.py
  A   zc.objectlog/trunk/src/zc/objectlog/utils.py

-=-
Added: zc.objectlog/trunk/src/zc/objectlog/DEPENDENCIES.cfg
===================================================================
--- zc.objectlog/trunk/src/zc/objectlog/DEPENDENCIES.cfg	2006-08-15 21:08:39 UTC (rev 69540)
+++ zc.objectlog/trunk/src/zc/objectlog/DEPENDENCIES.cfg	2006-08-15 21:11:43 UTC (rev 69541)
@@ -0,0 +1 @@
+zc.security


Property changes on: zc.objectlog/trunk/src/zc/objectlog/DEPENDENCIES.cfg
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.objectlog/trunk/src/zc/objectlog/__init__.py
===================================================================
--- zc.objectlog/trunk/src/zc/objectlog/__init__.py	2006-08-15 21:08:39 UTC (rev 69540)
+++ zc.objectlog/trunk/src/zc/objectlog/__init__.py	2006-08-15 21:11:43 UTC (rev 69541)
@@ -0,0 +1,21 @@
+##############################################################################
+#
+# Copyright (c) 2005 Zope Corporation. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Visible Source
+# License, Version 1.0 (ZVSL).  A copy of the ZVSL 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.
+#
+##############################################################################
+"""objectlog package
+
+$Id: __init__.py 901 2005-03-08 14:19:44Z gary $
+"""
+
+from log import Log
+from interfaces import ILog, ILogEntry, IRecord, ILogging

Added: zc.objectlog/trunk/src/zc/objectlog/browser/__init__.py
===================================================================
--- zc.objectlog/trunk/src/zc/objectlog/browser/__init__.py	2006-08-15 21:08:39 UTC (rev 69540)
+++ zc.objectlog/trunk/src/zc/objectlog/browser/__init__.py	2006-08-15 21:11:43 UTC (rev 69541)
@@ -0,0 +1 @@
+#

Added: zc.objectlog/trunk/src/zc/objectlog/browser/configure.zcml
===================================================================
--- zc.objectlog/trunk/src/zc/objectlog/browser/configure.zcml	2006-08-15 21:08:39 UTC (rev 69540)
+++ zc.objectlog/trunk/src/zc/objectlog/browser/configure.zcml	2006-08-15 21:11:43 UTC (rev 69541)
@@ -0,0 +1,26 @@
+<configure
+    xmlns="http://namespaces.zope.org/zope"
+    xmlns:browser="http://namespaces.zope.org/browser"
+    xmlns:zc="http://namespaces.zope.com/zc"
+    >
+
+  <adapter factory=".log.default_template" name="default" />
+
+  <adapter factory=".log.LogView" name="log.html"
+           provides="zope.publisher.interfaces.browser.IBrowserPublisher" />
+
+  <browser:menuItem action="log.html" for="zc.objectlog.interfaces.ILogging"
+            title="Log" menu="zmi_views" />
+
+  <class class=".log.LogView">
+    <require permission="zope.Public"
+             attributes="__call__ context request update render" />
+    <require permission="zope.Public"
+             interface="zope.publisher.interfaces.browser.IBrowserPublisher" />
+  </class>
+
+  <class class=".log.AggregatedLogView">
+    <require like_class=".log.LogView"/>
+  </class>
+
+</configure>

Added: zc.objectlog/trunk/src/zc/objectlog/browser/default.pt
===================================================================
--- zc.objectlog/trunk/src/zc/objectlog/browser/default.pt	2006-08-15 21:08:39 UTC (rev 69540)
+++ zc.objectlog/trunk/src/zc/objectlog/browser/default.pt	2006-08-15 21:11:43 UTC (rev 69541)
@@ -0,0 +1,18 @@
+<html metal:use-macro="context/@@standard_macros/view"
+      i18n:domain="zc.intranet">
+<head>
+    <metal:block fill-slot="headers">
+    <script src="/@@/zc.table.js"></script>
+    </metal:block>
+</head>
+<body>
+<metal:block metal:fill-slot="body">
+  <form method="POST" action="" tal:attributes="action request/URL">
+    <div id="viewspace">
+      <h3 i18n:translate="">Log</h3>
+        <div tal:replace="structure view/formatter">table goes here</div>
+    </div> <!-- id="viewspace" -->
+  </form>
+</metal:block>
+</body>
+</html>

Added: zc.objectlog/trunk/src/zc/objectlog/browser/interfaces.py
===================================================================
--- zc.objectlog/trunk/src/zc/objectlog/browser/interfaces.py	2006-08-15 21:08:39 UTC (rev 69540)
+++ zc.objectlog/trunk/src/zc/objectlog/browser/interfaces.py	2006-08-15 21:11:43 UTC (rev 69541)
@@ -0,0 +1,22 @@
+##############################################################################
+#
+# Copyright (c) 2005 Zope Corporation. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Visible Source
+# License, Version 1.0 (ZVSL).  A copy of the ZVSL 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 zope.publisher.interfaces.browser import IBrowserPublisher
+
+class ILoggingView(IBrowserPublisher):
+    "A view for ILogging objects"
+    
+class IAggregatedLoggingView(ILoggingView): # XXX poorly defined. :-(
+    "An aggregated view of logs for an object"

Added: zc.objectlog/trunk/src/zc/objectlog/browser/log.py
===================================================================
--- zc.objectlog/trunk/src/zc/objectlog/browser/log.py	2006-08-15 21:08:39 UTC (rev 69540)
+++ zc.objectlog/trunk/src/zc/objectlog/browser/log.py	2006-08-15 21:11:43 UTC (rev 69541)
@@ -0,0 +1,167 @@
+##############################################################################
+#
+# Copyright (c) 2005 Zope Corporation. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Visible Source
+# License, Version 1.0 (ZVSL).  A copy of the ZVSL 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.
+#
+##############################################################################
+"""views for logs
+
+$Id: log.py 9509 2006-04-29 01:40:46Z gary $
+"""
+from zope import interface, component, i18n, proxy
+from zope.app import zapi
+from zope.interface.common.idatetime import ITZInfo
+from zope.publisher.interfaces.browser import IBrowserRequest
+from zope.app.pagetemplate.viewpagetemplatefile import ViewPageTemplateFile
+from zope.formlib import namedtemplate, form
+import zope.publisher.browser
+
+import zc.table.column
+import zc.table.interfaces
+
+from zc.objectlog import interfaces
+import zc.objectlog.browser.interfaces
+from zc.objectlog.i18n import _
+
+class SortableColumn(zc.table.column.GetterColumn):
+    interface.implements(zc.table.interfaces.ISortableColumn)
+
+def dateFormatter(value, context, formatter):
+    value = value.astimezone(ITZInfo(formatter.request))
+    dateFormatter = formatter.request.locale.dates.getFormatter(
+        'dateTime', length='medium')
+    return dateFormatter.format(value)
+
+def principalsGetter(context, formatter):
+    principals = zapi.principals()
+    return [principals.getPrincipal(pid) for pid in context.principal_ids]
+
+def principalsFormatter(value, context, formatter): 
+    return ', '.join([v.title for v in value])
+
+def logFormatter(value, context, formatter):
+    summary, details = value
+    res = []
+    if summary:
+        res.append('<div class="logSummary">%s</div>' % (
+            i18n.translate(summary,
+                           context=formatter.request,
+                           default=summary),))
+    if details:
+        res.append('<div class="details">%s</div>' % (
+            i18n.translate(details,
+                           context=formatter.request,
+                           default=details),))
+    if res:
+        return '\n'.join(res)
+    else:
+        return i18n.translate(
+            _('no_summary_or_details_available-log_view',
+              '[no information available]'), context=formatter.request)
+
+def changesGetter(item, formatter):
+    obj = form.FormData(item.record_schema, item.record_changes)
+    interface.directlyProvides(obj, item.record_schema)
+    return obj
+
+def recordGetter(item, formatter):
+    obj = form.FormData(item.record_schema, item.record)
+    interface.directlyProvides(obj, item.record_schema)
+    return obj
+
+def recordFormatter(value, item, formatter):
+    view = component.getMultiAdapter(
+        (value, item, formatter.request), name='logrecordview')
+    view.update()
+    return view.render()
+
+default_template = namedtemplate.NamedTemplateImplementation(
+    ViewPageTemplateFile('default.pt'),
+    zc.objectlog.browser.interfaces.ILoggingView)
+
+class LogView(zope.publisher.browser.BrowserPage):
+    interface.implements(
+        zc.objectlog.browser.interfaces.ILoggingView)
+    component.adapts( # could move to IAdaptableToLogging ;-)
+        interfaces.ILogging, IBrowserRequest)
+
+    template = namedtemplate.NamedTemplate('default')
+
+    columns = (
+        SortableColumn(_('log_column-date', 'Date'),
+                       lambda c, f: c.timestamp, dateFormatter),
+        SortableColumn(_('log_column-principals', 'Principals'),
+                       principalsGetter, principalsFormatter),
+        zc.table.column.GetterColumn(
+            _('log_column-log', 'Log'), lambda c, f: (c.summary, c.details),
+            logFormatter),
+        zc.table.column.GetterColumn(
+            _('log_column-details', 'Changes'), changesGetter, recordFormatter),
+    #    zc.table.column.GetterColumn(
+    #        _('log_column-details', 'Full Status'), recordGetter, recordFormatter)
+        )
+
+    def __init__(self, context, request):
+        self.context = context
+        self.request = request
+
+    def update(self):
+        formatter_factory = component.getUtility(
+            zc.table.interfaces.IFormatterFactory)
+        self.formatter = formatter_factory(
+            self, self.request, interfaces.ILogging(self.context).log,
+            columns=self.columns)
+
+    def render(self):
+        return self.template()
+
+    def __call__(self):
+        self.update()
+        return self.render()
+
+def objectGetter(item, formatter):
+    return item.__parent__.__parent__
+
+def objectFormatter(value, item, formatter):
+    view = component.getMultiAdapter(
+        (value, item, formatter), name='log source')
+    view.update()
+    return view.render()
+
+class AggregatedLogView(LogView):
+    interface.implements(
+        zc.objectlog.browser.interfaces.IAggregatedLoggingView)
+    component.adapts( # could move to IAdaptableToLogging ;-)
+        interfaces.ILogging, IBrowserRequest)
+
+    template = namedtemplate.NamedTemplate('default')
+
+    columns = (
+        SortableColumn(_('log_column-task', 'Source'),
+                       objectGetter, objectFormatter),
+        SortableColumn(_('log_column-date', 'Date'),
+                       lambda c, f: c.timestamp, dateFormatter),
+        SortableColumn(_('log_column-principals', 'Principals'),
+                       principalsGetter, principalsFormatter),
+        zc.table.column.GetterColumn(
+            _('log_column-log', 'Log'), lambda c, f: (c.summary, c.details),
+            logFormatter),
+        zc.table.column.GetterColumn(
+            _('log_column-details', 'Changes'),
+            changesGetter, recordFormatter),
+        )
+
+    def update(self):
+        formatter_factory = component.getUtility(
+            zc.table.interfaces.IFormatterFactory)
+        self.formatter = formatter_factory(
+            self, self.request, interfaces.IAggregatedLog(self.context),
+            columns=self.columns)

Added: zc.objectlog/trunk/src/zc/objectlog/configure.zcml
===================================================================
--- zc.objectlog/trunk/src/zc/objectlog/configure.zcml	2006-08-15 21:08:39 UTC (rev 69540)
+++ zc.objectlog/trunk/src/zc/objectlog/configure.zcml	2006-08-15 21:11:43 UTC (rev 69541)
@@ -0,0 +1,37 @@
+<configure xmlns="http://namespaces.zope.org/zope"
+           i18n_domain="zc.objectlog">
+
+  <class class=".utils.ImmutableDict">
+    <allow interface="zope.interface.common.mapping.IExtendedReadMapping
+                      zope.interface.common.mapping.IClonableMapping" />
+  </class>
+  
+  <class class=".log.Log">
+    <allow attributes="__call__" />
+    <require permission="zope.View"
+             interface="zope.interface.common.sequence.IFiniteSequence" />
+  </class>
+  
+  <class class=".log.LogEntry">
+    <allow interface=".interfaces.ILogEntry"/>
+  </class>
+
+  <adapter factory=".copier.objectlog_copyfactory" />
+
+  <include package=".browser"/>
+
+  <configure
+    xmlns:zcml="http://namespaces.zope.org/zcml"
+    zcml:condition="have apidoc"
+    xmlns="http://namespaces.zope.org/apidoc"
+    >
+
+    <bookchapter
+      id="zcobjectlogreadme.txt"
+      title="Object Log API"
+      doc_path="log.txt"
+      />
+
+  </configure> 
+
+</configure>

Added: zc.objectlog/trunk/src/zc/objectlog/copier.py
===================================================================
--- zc.objectlog/trunk/src/zc/objectlog/copier.py	2006-08-15 21:08:39 UTC (rev 69540)
+++ zc.objectlog/trunk/src/zc/objectlog/copier.py	2006-08-15 21:11:43 UTC (rev 69541)
@@ -0,0 +1,17 @@
+
+import zope.component
+import zope.interface
+import zc.copyversion.interfaces
+from zc.objectlog import interfaces, log
+
+ at zope.component.adapter(interfaces.ILog)
+ at zope.interface.implementer(zc.copyversion.interfaces.ICopyHook)
+def objectlog_copyfactory(original):
+    def factory(location, register):
+        obj = log.Log(original.record_schema)
+        def reparent(convert):
+            obj.__parent__ = convert(original.__parent__)
+            obj.__name__ = obj.__name__
+        register(reparent)
+        return obj
+    return factory
\ No newline at end of file

Added: zc.objectlog/trunk/src/zc/objectlog/copier.txt
===================================================================
--- zc.objectlog/trunk/src/zc/objectlog/copier.txt	2006-08-15 21:08:39 UTC (rev 69540)
+++ zc.objectlog/trunk/src/zc/objectlog/copier.txt	2006-08-15 21:11:43 UTC (rev 69541)
@@ -0,0 +1,53 @@
+The copier module provides an adapter that can be used with
+zc.copyversion.copier.copy (and thus the replacement ObjectCopier also in the
+same module) so that copies of objects with an object log get fresh logs.
+
+For instance, consider the following.  
+
+    >>> import zope.interface
+    >>> import zope.schema
+    >>> import zope.location
+    >>> import zc.objectlog
+    >>> class ICat(zope.interface.Interface):
+    ...     name = zope.schema.TextLine(title=u"Name", required=True)
+    ...
+    >>> import persistent
+    >>> class Cat(persistent.Persistent):
+    ...     zope.interface.implements(ICat)
+    ...     def __init__(self, name):
+    ...         self.name = name
+    ...         self.log = zc.objectlog.Log(ICat)
+    ...         zope.location.locate(self.log, self, 'log')
+    ...
+    >>> emily = Cat(u'Emily')
+    >>> len(emily.log)
+    0
+    >>> entry = emily.log(u'Said hello to Emily')
+    >>> len(emily.log)
+    1
+
+Without the adapter, in theory copying an object copies the log, and
+all of the entries.  This would be wasteful--logs can be very long--and
+incorrect, at least for many use cases--the clone is a new object, and
+should get a fresh objectlog.
+
+In reality, as of this writing, the problem is worse: the log cannot be copied
+because of a problem with unpickling an internal data structure.
+
+The adapter addresses all of the problems.
+
+    >>> import zope.component
+    >>> import zc.objectlog.copier
+    >>> import zc.copyversion.copier
+    >>> zope.component.provideAdapter(
+    ...     zc.objectlog.copier.objectlog_copyfactory)
+    >>> emily_clone = zc.copyversion.copier.copy(emily)
+    >>> len(emily_clone.log)
+    0
+    >>> emily_clone.log.__parent__ is emily_clone
+    True
+
+Just to make sure that emily hasn't been touched:
+
+    >>> len(emily.log)
+    1
\ No newline at end of file


Property changes on: zc.objectlog/trunk/src/zc/objectlog/copier.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.objectlog/trunk/src/zc/objectlog/i18n.py
===================================================================
--- zc.objectlog/trunk/src/zc/objectlog/i18n.py	2006-08-15 21:08:39 UTC (rev 69540)
+++ zc.objectlog/trunk/src/zc/objectlog/i18n.py	2006-08-15 21:11:43 UTC (rev 69541)
@@ -0,0 +1,32 @@
+##############################################################################
+#
+# Copyright (c) 2005 Zope Corporation. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Visible Source
+# License, Version 1.0 (ZVSL).  A copy of the ZVSL 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
+#
+##############################################################################
+"""I18N support for objectlog.
+
+This defines a `MessageFactory` for the I18N domain for the objectlog
+package.  This is normally used with this import::
+
+  from i18n import MessageFactory as _
+
+The factory is then used normally.  Two examples::
+
+  text = _('some internationalized text')
+  text = _('helpful-descriptive-message-id', 'default text')
+"""
+__docformat__ = "reStructuredText"
+
+
+from zope import i18nmessageid
+
+MessageFactory = _ = i18nmessageid.MessageFactory("zc.objectlog")

Added: zc.objectlog/trunk/src/zc/objectlog/interfaces.py
===================================================================
--- zc.objectlog/trunk/src/zc/objectlog/interfaces.py	2006-08-15 21:08:39 UTC (rev 69540)
+++ zc.objectlog/trunk/src/zc/objectlog/interfaces.py	2006-08-15 21:11:43 UTC (rev 69541)
@@ -0,0 +1,146 @@
+##############################################################################
+#
+# Copyright (c) 2005 Zope Corporation. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Visible Source
+# License, Version 1.0 (ZVSL).  A copy of the ZVSL 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.
+#
+##############################################################################
+"""objectlog package interfaces
+
+$Id: interfaces.py 9482 2006-04-28 04:44:31Z gary $
+"""
+from zope import interface, schema
+import zope.interface.common.sequence
+import zope.component.interfaces
+
+from zc.security.search import SimplePrincipalSource
+
+from i18n import _
+
+class ILog(zope.interface.common.sequence.IFiniteSequence):
+    def __call__(summary=None, details=None, defer=False, if_changed=False):
+        """add an ILogEntry to logged with optional details, summary, and data.
+
+        details should be a schema.Text field; summary should be a
+        schema.TextLine field.  The details and summary fields will move to
+        rich text fields when they are available.
+
+        Adapts self.__parent__ to self.record_schema, checks the adapted value
+        to see if it validates with the schema, compares the current
+        values with the last logged values, and creates a log entry with the
+        change set, the current record_schema, the summary, the details, and
+        the data.
+
+        If defer is True, will defer making the log entry until the end of the
+        transaction (a non-guaranteed point within the transaction's
+        beforeCommitHooks, but before other subsequent deferred log calls).
+
+        If if_changed is True, a log will only be made if a change has been
+        made since the last log entry.
+
+        If both defer and if_changed are True, any other log entries that are
+        deferred but not if_changed will come first, effectively eliminating
+        all deferred, if_changed entries.  Similarly, if there are no deferred,
+        non-if_changed entries, only the first requested if_changed log will
+        be made.
+        """
+
+    record_schema = schema.InterfaceField(
+        title=_("Record Schema"),
+        description=_("""The schema used for creating log entry records.
+
+        May be altered with a schema that extends the last-used schema.
+
+        Non-schema specifications (e.g., interface.Attribute and methods) are
+        ignored.
+        """),
+        required=True)
+
+class ILogging(interface.Interface):
+    "An object which provides an ILog as a 'log' attribute"
+
+    log = schema.Object(ILog, title=_('Log'), description=_(
+                        "A zc.objectlog.ILog"), readonly=True, required=True)
+
+class IRecord(interface.Interface):
+    """Data about the logged object when the log entry was made.
+
+    Records always implement an additional interface: the record_schema of the
+    corresponding log entry."""
+
+class ILogEntry(interface.Interface):
+    """A log entry.
+
+    Log entries have three broad use cases:
+    - Record transition change messages from system users
+    - Record basic status values so approximate change timestamps can
+      be calculated
+    - Allow for simple extensibility.
+    """
+    timestamp = schema.Datetime(
+        title=_("Creation Date"),
+        description=_("The date and time at which this log entry was made"),
+        required=True, readonly=True)
+
+    principal_ids = schema.Tuple(
+        value_type=schema.Choice(
+            source=SimplePrincipalSource()),
+        title=_("Principals"),
+        description=_(
+            """The ids of the principals who triggered this log entry"""),
+        required=True, readonly=True)
+
+    summary = schema.TextLine( # XXX Make rich text line later
+        title=_("Summary"),
+        description=_("Log summary"),
+        required=False, readonly=True)
+
+    details = schema.Text( # XXX Make rich text later
+        title=_("Details"),
+        description=_("Log details"),
+        required=False, readonly=True)
+
+    record_schema = schema.InterfaceField(
+        title=_("Record Schema"),
+        description=_("""The schema used for creating log entry records.
+
+        Non-schema specifications (e.g., interface.Attribute and methods) are
+        ignored."""),
+        required=True, readonly=True)
+
+    record_changes = schema.Object(
+        zope.interface.common.mapping.IExtendedReadMapping,
+        title=_("Changes"),
+        description=_("Changes to the object since the last log entry"),
+        required=True, readonly=True)
+
+    record = schema.Object(
+        IRecord,
+        title=_("Full Status"),
+        description=_("The status of the object at this log entry"),
+        required=True, readonly=True)
+
+    next = interface.Attribute("The next log entry, or None if last")
+
+    previous = interface.Attribute("The previous log entry, or None if first")
+
+class IAggregatedLog(interface.Interface):
+    """an iterable of logs aggregated for a given object"""
+
+class ILogEntryEvent(zope.component.interfaces.IObjectEvent):
+    """object is log's context (__parent__)"""
+
+    entry = interface.Attribute('the log entry created')
+
+class LogEntryEvent(zope.component.interfaces.ObjectEvent):
+    interface.implements(ILogEntryEvent)
+    def __init__(self, obj, entry):
+        super(LogEntryEvent, self).__init__(obj)
+        self.entry = entry

Added: zc.objectlog/trunk/src/zc/objectlog/log.py
===================================================================
--- zc.objectlog/trunk/src/zc/objectlog/log.py	2006-08-15 21:08:39 UTC (rev 69540)
+++ zc.objectlog/trunk/src/zc/objectlog/log.py	2006-08-15 21:11:43 UTC (rev 69541)
@@ -0,0 +1,248 @@
+##############################################################################
+#
+# Copyright (c) 2005 Zope Corporation. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Visible Source
+# License, Version 1.0 (ZVSL).  A copy of the ZVSL 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.
+#
+##############################################################################
+"""objectlog implementation
+
+$Id: log.py 9482 2006-04-28 04:44:31Z gary $
+"""
+import pytz, datetime, persistent, transaction
+from BTrees import IOBTree
+import zope.security.management
+import zope.security.interfaces
+import zope.security.checker
+import zope.security.proxy
+from zope import interface, schema, event
+import zope.interface.interfaces
+import zope.location
+import zope.app.keyreference.interfaces
+
+from zc.objectlog import interfaces, utils
+from zc.objectlog.i18n import _
+
+def performDeferredLogs(deferred, seen, transaction):
+    # deferred is dict of 
+    # (parent key reference, log name): (not if_changed list, if_changed list)
+    # where not if_changed and if_changed are both lists of
+    # [(log, summary, details)]
+    problem = False
+    for hook, args, kwargs in reversed(
+        tuple(transaction.getBeforeCommitHooks())):
+        if hook is performDeferredLogs:
+            break
+        else:
+            if (hook, args, kwargs) not in seen:
+                seen.append((hook, args, kwargs))
+                problem = True
+    if problem:
+        transaction.addBeforeCommitHook(
+            performDeferredLogs, (deferred, seen, transaction))
+    else:
+        for always, if_changed in deferred.values():
+            if always:
+                for log, summary, details in always:
+                    log._call(summary, details)
+            else:
+                log, summary, details = if_changed[0]
+                log._call(summary, details, if_changed=True)
+
+def getTransactionFromPersistentObject(obj):
+    # this should maybe go in ZODB; extracted from some of Jim's code
+    connection = obj._p_jar
+    if connection is None:
+        return False
+    try:
+        tm = connection._txn_mgr
+    except AttributeError:
+        tm = connection.transaction_manager
+    return tm.get()
+
+class Log(persistent.Persistent, zope.location.Location):
+    interface.implements(interfaces.ILog)
+
+    def __init__(self, record_schema):
+        self.entries = IOBTree.IOBTree()
+        self._record_schema = record_schema
+
+    def record_schema(self, value):
+        if self.entries:
+            last_schema = self[-1].record_schema
+            if value is not last_schema and not value.extends(last_schema):
+                raise ValueError(
+                    _("Once entries have been made, may only change schema to "
+                      "one that extends the last-used schema"))
+        self._record_schema = value
+    record_schema = property(lambda self: self._record_schema, record_schema)
+
+    def __call__(self, 
+                 summary=None, details=None, defer=False, if_changed=False):
+        if defer:
+            o = self.__parent__
+            key = (zope.app.keyreference.interfaces.IKeyReference(o),
+                   self.__name__)
+            t = getTransactionFromPersistentObject(self) or transaction.get()
+            for hook, args, kwargs in t.getBeforeCommitHooks():
+                if hook is performDeferredLogs:
+                    deferred = args[0]
+                    ds = deferred.get(key)
+                    if ds is None:
+                        ds = deferred[key] = ([], [])
+                    break
+            else:
+                ds = ([], [])
+                deferred = {key: ds}
+                t.addBeforeCommitHook(performDeferredLogs, (deferred, [], t))
+            ds[bool(if_changed)].append((self, summary, details))
+        else:
+            return self._call(summary, details, if_changed=if_changed)
+
+    def _call(self, summary, details, if_changed=False):
+        s = self.record_schema
+        new_record = s(self.__parent__)
+        utils.validate(new_record, s)
+        entries_len = len(self)
+        changes = {}
+        if entries_len:
+            old_record = self[-1].record
+            for name, field in schema.getFieldsInOrder(s):
+                old_val = field.query(old_record, field.missing_value)
+                new_val = field.query(new_record, field.missing_value)
+                if new_val != old_val:
+                    changes[name] = new_val
+        else:
+            for name, field in schema.getFieldsInOrder(s):
+                changes[name] = field.query(new_record, field.missing_value)
+        if not if_changed or changes:
+            new = LogEntry(
+                entries_len, changes, self.record_schema, summary, details)
+            zope.location.locate(new, self, unicode(entries_len))
+            utils.validate(new, interfaces.ILogEntry)
+            self.entries[entries_len] = new
+            event.notify(interfaces.LogEntryEvent(self.__parent__, new))
+            return new
+        # else return None
+
+    def __getitem__(self, ix):
+        if isinstance(ix, slice):
+            indices = ix.indices(len(self))
+            return [self.entries[i] for i in range(*indices)]
+        # XXX put this in traversal adapter (I think)
+        if isinstance(ix, basestring):
+            ix = int(ix)
+        if ix < 0:
+            ix = len(self) + ix
+        try:
+            return self.entries[ix]
+        except KeyError:
+            raise IndexError, 'list index out of range'
+    
+    def __len__(self):
+        if self.entries:
+            return self.entries.maxKey() + 1
+        else:
+            return 0
+
+    def __iter__(self):
+        for l in self.entries.values():
+            yield l
+
+class LogEntry(persistent.Persistent, zope.location.Location):
+    interface.implements(interfaces.ILogEntry)
+
+    def __init__(self, ix, record_changes, record_schema, 
+                 summary, details):
+        self.index = ix
+        self.record_changes = utils.ImmutableDict(record_changes)
+        self.record_schema = record_schema
+        self.summary = summary
+        self.details = details
+        self.timestamp = datetime.datetime.now(pytz.utc)
+        try:
+            interaction = zope.security.management.getInteraction()
+        except zope.security.interfaces.NoInteraction:
+            self.principal_ids = ()
+        else:
+            self.principal_ids = tuple(
+                [p.principal.id for p in interaction.participations
+                 if zope.publisher.interfaces.IRequest.providedBy(p)])
+    
+    record = property(lambda self: Record(self))
+
+    def next(self):
+        try:
+            return self.__parent__[self.index+1]
+        except IndexError:
+            return None
+    next = property(next)
+    
+    def previous(self):
+        ix = self.index
+        if ix:
+            return self.__parent__[ix-1]
+        else: # it's 0
+            return None
+    previous = property(previous)
+
+class RecordChecker(object):
+    interface.implements(zope.security.interfaces.IChecker)
+
+    def check_setattr(self, obj, name):
+        raise zope.security.interfaces.ForbiddenAttribute, (name, obj)
+
+    def check(self, obj, name):
+        if name not in zope.security.checker._available_by_default:
+            entry = zope.security.proxy.removeSecurityProxy(obj.__parent__)
+            schema = entry.record_schema
+            if name not in schema:
+                raise zope.security.interfaces.ForbiddenAttribute, (name, obj)
+    check_getattr = __setitem__ = check
+
+    def proxy(self, value):
+        'See IChecker'
+        checker = getattr(value, '__Security_checker__', None)
+        if checker is None:
+            checker = zope.security.checker.selectChecker(value)
+            if checker is None:
+                return value
+        return zope.security.checker.Proxy(value, checker)
+
+class Record(zope.location.Location): # not intended to be persistent
+    interface.implements(interfaces.IRecord)
+    
+    __name__ = u"record"
+    
+    __Security_checker__ = RecordChecker()
+    
+    def __init__(self, entry):
+        self.__parent__ = entry
+        interface.directlyProvides(self, entry.record_schema)
+
+    def __getattr__(self, name):
+        entry = self.__parent__
+        schema = entry.record_schema
+        try:
+            field = schema[name]
+        except KeyError:
+            raise AttributeError, name
+        else:
+            while entry is not None:
+                if name in entry.record_changes:
+                    v = value = entry.record_changes[name]
+                    break
+                entry = entry.previous
+            else: # we currently can never get here
+                v = value = getattr(schema[name], 'missing_value', None)
+            if zope.interface.interfaces.IMethod.providedBy(field):
+                v = lambda : value
+            setattr(self, name, v)
+            return v

Added: zc.objectlog/trunk/src/zc/objectlog/log.txt
===================================================================
--- zc.objectlog/trunk/src/zc/objectlog/log.txt	2006-08-15 21:08:39 UTC (rev 69540)
+++ zc.objectlog/trunk/src/zc/objectlog/log.txt	2006-08-15 21:11:43 UTC (rev 69541)
@@ -0,0 +1,578 @@
+=========
+objectlog
+=========
+
+The objectlog package provides a customizable log for a single object.  The 
+system was designed to provide information for a visual log of important
+object changes and to provide analyzable information for metrics.  
+
+- It provides automatic recording for each log entry of a timestamp and the 
+  principals in the request when the log was made.
+
+- Given a schema of data to collect about the object, it automatically 
+  calculates and stores changesets from the last log entry, primarily to 
+  provide a quick and easy answer to the question "what changed?" and
+  secondarily to reduce database size.
+
+- It accepts optional summary and detail values that allow the system or users 
+  to annotate the entries with human-readable messages.
+
+- It allows each log entry to be annotated with zero or more marker interfaces
+  so that log entries may be classified with an interface.
+
+Moreover, the log entries can be set to occur at transition boundaries, and
+to only ocur if a change was made (according to the changeset) since the last
+log entry.
+  
+To show this, we need to set up a dummy interaction.  We do this below, then
+create an object with a log, then actually make a log.
+
+    >>> import zope.security.management
+    >>> import zope.security.interfaces
+    >>> import zope.app.security.interfaces
+    >>> from zope import interface, schema
+    >>> from zope.app.testing import ztapi
+    >>> class DummyPrincipal(object):
+    ...     interface.implements(zope.security.interfaces.IPrincipal)
+    ...     def __init__(self, id, title, description):
+    ...         self.id = id
+    ...         self.title = title
+    ...         self.description = description
+    ...
+    >>> alice = DummyPrincipal('alice', 'Alice Aal', 'first principal')
+    >>> betty = DummyPrincipal('betty', 'Betty Barnes', 'second principal')
+    >>> cathy = DummyPrincipal('cathy', 'Cathy Camero', 'third principal')
+    >>> class DummyParticipation(object):
+    ...     interface.implements(zope.security.interfaces.IParticipation)
+    ...     interaction = principal = None
+    ...     def __init__(self, principal):
+    ...         self.principal = principal
+    ...
+    >>> class DummyAuthService(object):
+    ...     interface.implements(zope.app.security.interfaces.IAuthentication)
+    ...     def __init__(self, data):
+    ...         self.data = data
+    ...     def getPrincipal(self, id):
+    ...         return self.data[id]
+    ...
+    >>> auth = DummyAuthService(
+    ...     {'alice': alice, 'betty': betty, 'cathy': cathy})
+    >>> ztapi.provideUtility(
+    ...     zope.app.security.interfaces.IAuthentication, auth)
+    >>> import zope.publisher.interfaces
+    
+    >>> import zc.objectlog
+    >>> import zope.location
+    >>> WORKING = u"Where I'm working"
+    >>> COUCH = u"On couch"
+    >>> BED = u"On bed"
+    >>> KITCHEN = u"In kitchen"
+    >>> class ICat(interface.Interface):
+    ...     name = schema.TextLine(title=u"Name", required=True)
+    ...     location = schema.Choice(
+    ...         (WORKING, COUCH, BED, KITCHEN),
+    ...         title=u"Location", required=False)
+    ...     weight = schema.Int(title=u"Weight in Pounds", required=True)
+    ...     getAge, = schema.accessors(
+    ...         schema.Int(title=u"Age in Years", readonly=True, 
+    ...         required=False))
+    ...
+    >>> import persistent
+    >>> class Cat(persistent.Persistent):
+    ...     interface.implements(ICat)
+    ...     def __init__(self, name, weight, age, location=None):
+    ...         self.name = name
+    ...         self.weight = weight
+    ...         self.location = location
+    ...         self._age = age
+    ...         self.log = zc.objectlog.Log(ICat)
+    ...         zope.location.locate(self.log, self, 'log')
+    ...     def getAge(self):
+    ...         return self._age
+    ...
+
+Notice in the __init__ for cat that we located the log on the cat.  This is
+an important step, as it enables the automatic changesets.
+
+Now we are set up to look at examples.  With one exception, each example
+runs within a faux interaction so we can see how the principal_ids
+attribute works.  First we'll see that len works, that the record_schema
+attribute is set properly, that the timestamp uses a pytz.utc timezone for
+the timestamp, that log iteration works, and that summary, details, and data
+were set properly.
+
+    >>> import pytz, datetime
+    >>> a_p = DummyParticipation(alice)
+    >>> interface.directlyProvides(a_p, zope.publisher.interfaces.IRequest)
+    >>> zope.security.management.newInteraction(a_p)
+    >>> emily = Cat(u'Emily', 16, 5, WORKING)
+    >>> len(emily.log)
+    0
+    >>> emily.log.record_schema is ICat
+    True
+    >>> before = datetime.datetime.now(pytz.utc)
+    >>> entry = emily.log(
+    ...     u'Starting to keep track of Emily', 
+    ...     u'Looks like\nshe might go upstairs soon')
+    >>> entry is emily.log[0]
+    True
+    >>> after = datetime.datetime.now(pytz.utc)
+    >>> len(emily.log)
+    1
+    >>> before <= entry.timestamp <= after
+    True
+    >>> entry.timestamp.tzinfo is pytz.utc
+    True
+    >>> entry.principal_ids
+    ('alice',)
+    >>> list(emily.log) == [entry]
+    True
+    >>> entry.record_schema is ICat
+    True
+    >>> entry.summary
+    u'Starting to keep track of Emily'
+    >>> entry.details
+    u'Looks like\nshe might go upstairs soon'
+
+The record and the record_changes should have a full set of values from the
+object.  The record has a special security checker that allows users to
+access any field defined on the schema, but not to access any others nor to
+write any values.
+
+    >>> record = emily.log[0].record
+    >>> record.name
+    u'Emily'
+    >>> record.location==WORKING
+    True
+    >>> record.weight
+    16
+    >>> record.getAge()
+    5
+    >>> ICat.providedBy(record)
+    True
+    >>> emily.log[0].record_changes == {
+    ... 'name': u'Emily', 'weight': 16, 'location': u"Where I'm working",
+    ... 'getAge': 5}
+    True
+    >>> from zope.security.checker import ProxyFactory
+    >>> proxrecord = ProxyFactory(record)
+    >>> ICat.providedBy(proxrecord)
+    True
+    >>> from zc.objectlog import interfaces
+    >>> interfaces.IRecord.providedBy(proxrecord)
+    True
+    >>> from zope.security import canAccess, canWrite
+    >>> canAccess(record, 'name')
+    True
+    >>> canAccess(record, 'weight')
+    True
+    >>> canAccess(record, 'location')
+    True
+    >>> canAccess(record, 'getAge')
+    True
+    >>> canAccess(record, 'shazbot') # doctest: +ELLIPSIS
+    Traceback (most recent call last):
+    ...
+    ForbiddenAttribute: ('shazbot', ...
+    >>> canWrite(record, 'name')
+    False
+    >>> zope.security.management.endInteraction()
+
+Interactions with multiple principals are correctly recorded as well.  Note
+that non-request participations are not included in the records.  We also
+look a bit more at the record and the change set.
+
+    >>> a_p = DummyParticipation(alice)
+    >>> b_p = DummyParticipation(betty)
+    >>> c_p = DummyParticipation(cathy)
+    >>> interface.directlyProvides(a_p, zope.publisher.interfaces.IRequest)
+    >>> interface.directlyProvides(b_p, zope.publisher.interfaces.IRequest)
+    >>> zope.security.management.newInteraction(a_p, b_p, c_p)
+    >>> emily.location = KITCHEN
+    >>> entry = emily.log(u"Sounds like she's eating", u"Dry food,\nin fact.")
+    >>> len(emily.log)
+    2
+    >>> emily.log[0].summary
+    u'Starting to keep track of Emily'
+    >>> emily.log[1].summary
+    u"Sounds like she's eating"
+    >>> after <= emily.log[1].timestamp <= datetime.datetime.now(pytz.utc)
+    True
+    >>> emily.log[1].principal_ids # cathy was not a request, so not included
+    ('alice', 'betty')
+    >>> emily.log[1].details
+    u'Dry food,\nin fact.'
+    >>> emily.log[1].record_changes
+    {'location': u'In kitchen'}
+    >>> record = emily.log[1].record
+    >>> record.location
+    u'In kitchen'
+    >>> record.name
+    u'Emily'
+    >>> record.weight
+    16
+    >>> zope.security.management.endInteraction()
+
+It is possible to make a log without an interaction as well.
+
+    >>> emily._age = 6
+    >>> entry = emily.log(u'Happy Birthday') # no interaction
+    >>> len(emily.log)
+    3
+    >>> emily.log[2].principal_ids
+    ()
+    >>> emily.log[2].record_changes
+    {'getAge': 6}
+    >>> record = emily.log[2].record
+    >>> record.location
+    u'In kitchen'
+    >>> record.name
+    u'Emily'
+    >>> record.weight
+    16
+    >>> record.getAge()
+    6
+
+Entries may be marked with marker interfaces to categorize them.  This approach
+may be difficult with security proxies, so it may be changed.  We'll do all
+the rest of our examples within the same interaction.
+
+    >>> c_p = DummyParticipation(cathy)
+    >>> interface.directlyProvides(c_p, zope.publisher.interfaces.IRequest)
+    >>> zope.security.management.newInteraction(c_p)
+    >>> emily.location = None
+    >>> emily.weight = 17
+    >>> class IImportantLogEntry(interface.Interface):
+    ...     "A marker interface for log entries"
+    >>> interface.directlyProvides(
+    ...     emily.log(u'Emily is in transit...and ate a bit too much'),
+    ...     IImportantLogEntry)
+    >>> len(emily.log)
+    4
+    >>> [e for e in emily.log if IImportantLogEntry.providedBy(e)] == [
+    ...     emily.log[3]]
+    True
+    >>> emily.log[3].principal_ids
+    ('cathy',)
+    >>> emily.log[3].record_changes=={'weight': 17, 'location': None}
+    True
+    >>> record = emily.log[3].record
+    >>> old_record = emily.log[2].record
+    >>> record.name == old_record.name == u'Emily'
+    True
+    >>> record.weight
+    17
+    >>> old_record.weight
+    16
+    >>> record.location # None
+    >>> old_record.location
+    u'In kitchen'
+
+Making a log will fail if the record it is trying to make does not conform
+to its schema.
+
+    >>> emily.location = u'Outside'
+    >>> emily.log(u'This should never happen')
+    Traceback (most recent call last):
+    ...
+    ConstraintNotSatisfied: Outside
+    >>> len(emily.log)
+    4
+    >>> emily.location = BED
+
+It will also fail if the arguments passed to it are not correct.
+
+    >>> emily.log("This isn't unicode so will not succeed")
+    Traceback (most recent call last):
+    ...
+    WrongType: ("This isn't unicode so will not succeed", <type 'unicode'>)
+    >>> len(emily.log)
+    4
+    >>> success = emily.log(u"Yay, unicode")
+
+The following is commented out until we have more 
+
+    # >>> emily.log(u"Data without an interface won't work", None, 'boo hoo')
+    Traceback (most recent call last):
+    ...
+    WrongContainedType: []
+
+Zero or more additional arbitrary data objects may be included on the log entry
+as long as they implement an interface.
+
+    >>> class IConsumableRecord(interface.Interface):
+    ...     dry_food = schema.Int(
+    ...         title=u"Dry found consumed in ounces", required=False)
+    ...     wet_food = schema.Int(
+    ...         title=u"Wet food consumed in ounces", required=False)
+    ...     water = schema.Int(
+    ...         title=u"Water consumed in teaspoons", required=False)
+    ...
+
+    # >>> class ConsumableRecord(object):
+    ...     interface.implements(IConsumableRecord)
+    ...     def __init__(self, dry_food=None, wet_food=None, water=None):
+    ...         self.dry_food = dry_food
+    ...         self.wet_food = wet_food
+    ...         self.water = water
+    ...
+    # >>> entry = emily.log(u'Collected eating records', None, ConsumableRecord(1))
+    # >>> len(emily.log)
+    5
+    # >>> len(emily.log[4].data)
+    1
+    # >>> IConsumableRecord.providedBy(emily.log[4].data[0])
+    True
+    # >>> emily.log[4].data[0].dry_food
+    1
+
+__getitem__ and __iter__ work as normal for a Python sequence, including 
+support for extended slices.
+
+    >>> list(emily.log) == [emily.log[0], emily.log[1], emily.log[2],
+    ...                     emily.log[3], emily.log[4]]
+    True
+    >>> emily.log[-1] is emily.log[4]
+    True
+    >>> emily.log[0] is emily.log[-5]
+    True
+    >>> emily.log[5]
+    Traceback (most recent call last):
+    ...
+    IndexError: list index out of range
+    >>> emily.log[-6]
+    Traceback (most recent call last):
+    ...
+    IndexError: list index out of range
+    >>> emily.log[4:2:-1] == [emily.log[4], emily.log[3]]
+    True
+
+The log's record_schema may be changed as long as there are no logs or the
+interface extends (or is) the interface for the last log.
+
+    >>> emily.log.record_schema = IConsumableRecord # doctest: +ELLIPSIS
+    Traceback (most recent call last):
+    ...
+    ValueError: Once entries have been made, may only change schema to one...
+    >>> class IExtendedCat(ICat):
+    ...     parent_object_intid = schema.Int(title=u"Parent Object")
+    ...
+    >>> emily.log.record_schema = IExtendedCat
+    >>> emily.log.record_schema = ICat
+    >>> emily.log.record_schema = IExtendedCat
+    >>> class ExtendedCatAdapter(object):
+    ...     interface.implements(IExtendedCat)
+    ...     def __init__(self, cat): # getAge is left off
+    ...         self.name = cat.name
+    ...         self.weight = cat.weight
+    ...         self.location = cat.location
+    ...         self.parent_object_intid = 42
+    ...
+    >>> ztapi.provideAdapter((ICat,), IExtendedCat, ExtendedCatAdapter)
+    >>> entry = emily.log(u'First time with extended interface')
+    >>> emily.log.record_schema = ICat # doctest: +ELLIPSIS
+    Traceback (most recent call last):
+    ...
+    ValueError: Once entries have been made, may only change schema to one...
+    >>> emily.log[5].record_changes == {
+    ...     'parent_object_intid': 42, 'getAge': None}
+    True
+    >>> record = emily.log[5].record
+    >>> record.parent_object_intid
+    42
+    >>> record.name
+    u'Emily'
+    >>> record.location
+    u'On bed'
+    >>> record.weight
+    17
+    >>> record.getAge() # None
+    >>> IExtendedCat.providedBy(record)
+    True
+    >>> old_record = emily.log[3].record
+    >>> IExtendedCat.providedBy(old_record)
+    False
+    >>> ICat.providedBy(old_record)
+    True
+    >>> old_record.parent_object_intid # doctest: +ELLIPSIS
+    Traceback (most recent call last):
+    ...
+    AttributeError: ...
+
+Entries support convenience next and previous attributes, which make them
+act like immutable doubly linked lists:
+
+    >>> entry = emily.log[5]
+    >>> entry.previous is emily.log[4]
+    True
+    >>> entry.next # None
+    >>> entry.previous.previous.previous.previous.previous is emily.log[0]
+    True
+    >>> emily.log[0].previous # None
+    >>> emily.log[0].next is emily.log[1]
+    True
+
+Objectlogs also support deferring until the end of a transaction.  To show 
+this, we will need a sample database, a transaction, and key reference 
+adapters.  We show the simplest example first.
+
+    >>> from ZODB.tests import util
+    >>> import transaction
+
+    >>> db = util.DB()
+    >>> connection = db.open()
+    >>> root = connection.root()
+    >>> root["emily"] = emily
+    >>> transaction.commit()
+    >>> import zope.app.keyreference.persistent
+    >>> import zope.app.keyreference.interfaces
+    >>> import ZODB.interfaces
+    >>> import persistent.interfaces
+    >>> from zope import component
+    >>> component.provideAdapter(
+    ...     zope.app.keyreference.persistent.KeyReferenceToPersistent,
+    ...     (persistent.interfaces.IPersistent,),
+    ...     zope.app.keyreference.interfaces.IKeyReference)
+    >>> component.provideAdapter(
+    ...     zope.app.keyreference.persistent.connectionOfPersistent,
+    ...     (persistent.interfaces.IPersistent,),
+    ...     ZODB.interfaces.IConnection)
+
+    >>> len(emily.log)
+    6
+    >>> emily.log(u'This one is deferred', defer=True) # returns None: deferred!
+    >>> len(emily.log)
+    6
+    >>> transaction.commit()
+    >>> len(emily.log)
+    7
+    >>> emily.log[6].summary
+    u'This one is deferred'
+    >>> emily.log[6].record_changes
+    {}
+
+While this is interesting, the point is to capture changes to the object, 
+whether or not they happened when the log was called.  Here is a more pertinent
+example, then.
+
+    >>> len(emily.log)
+    7
+    >>> emily.weight = 16
+    >>> emily.log(u'Also deferred', defer=True) # returns None: deferred!
+    >>> len(emily.log)
+    7
+    >>> emily.location = COUCH
+    >>> transaction.commit()
+    >>> len(emily.log)
+    8
+    >>> emily.log[7].summary
+    u'Also deferred'
+    >>> import pprint
+    >>> pprint.pprint(emily.log[7].record_changes)
+    {'location': u'On couch', 'weight': 16}
+
+Multiple deferred log entries can be deferred, if desired.
+
+    >>> emily.log(u'One log', defer=True)
+    >>> emily.log(u'Two log', defer=True)
+    >>> len(emily.log)
+    8
+    >>> transaction.commit()
+    >>> len(emily.log)
+    10
+    >>> emily.log[8].summary
+    u'One log'
+    >>> emily.log[9].summary
+    u'Two log'
+
+Another option is if_changed.  It should not make a log unless there was a 
+change.
+
+    >>> len(emily.log)
+    10
+    >>> emily.log(u'If changed', if_changed=True) # returns None: no change!
+    >>> len(emily.log)
+    10
+    >>> emily.location = BED
+    >>> entry = emily.log(u'If changed', if_changed=True)
+    >>> len(emily.log)
+    11
+    >>> emily.log[10] is entry
+    True
+    >>> entry.summary
+    u'If changed'
+    >>> pprint.pprint(entry.record_changes)
+    {'location': u'On bed'}
+    >>> transaction.commit()
+
+The two options, if_changed and defer, can be used together.  This makes for
+a log entry that will only be made at a transition boundary if there have
+been no previous changes.  Note that a log entry that occurs whether or not
+changes were made (hereafter called a "required" log entry) that is also 
+deferred will always eliminate any deferred is_changed log entry, even if the
+required log entry was registered later in the transaction.
+
+    >>> len(emily.log)
+    11
+    >>> emily.log(u'Another', defer=True, if_changed=True) # returns None
+    >>> transaction.commit()
+    >>> len(emily.log)
+    11
+    >>> emily.log(u'Yet another', defer=True, if_changed=True) # returns None
+    >>> emily.location = COUCH
+    >>> len(emily.log)
+    11
+    >>> transaction.commit()
+    >>> len(emily.log)
+    12
+    >>> emily.log[11].summary
+    u'Yet another'
+    >>> emily.location = KITCHEN
+    >>> entry = emily.log(u'non-deferred entry', if_changed=True)
+    >>> len(emily.log)
+    13
+    >>> entry.summary
+    u'non-deferred entry'
+    >>> emily.log(u'will not write', defer=True, if_changed=True)
+    >>> transaction.commit()
+    >>> len(emily.log)
+    13
+    >>> emily.log(u'will not write', defer=True, if_changed=True)
+    >>> emily.location = WORKING
+    >>> emily.log(u'also will not write', defer=True, if_changed=True)
+    >>> emily.log(u'required, deferred', defer=True)
+    >>> len(emily.log)
+    13
+    >>> transaction.commit()
+    >>> len(emily.log)
+    14
+    >>> emily.log[13].summary
+    u'required, deferred'
+
+This should all work in the presence of multiple objects, of course.
+
+    >>> sam = Cat(u'Sam', 20, 4)
+    >>> root['sam'] = sam
+    >>> transaction.commit()
+    >>> sam.weight = 19
+    >>> sam.log(u'Auto log', defer=True, if_changed=True)
+    >>> sam.log(u'Sam lost weight!', defer=True)
+    >>> sam.log(u'Saw sam today', defer=True)
+    >>> emily.log(u'Auto log', defer=True, if_changed=True)
+    >>> emily.weight = 15
+    >>> transaction.commit()
+    >>> len(sam.log)
+    2
+    >>> sam.log[0].summary
+    u'Sam lost weight!'
+    >>> sam.log[1].summary
+    u'Saw sam today'
+    >>> len(emily.log)
+    15
+    >>> emily.log[14].summary
+    u'Auto log'
+
+    >>> # TEAR DOWN
+    >>> zope.security.management.endInteraction()
+    >>> ztapi.unprovideUtility(zope.app.security.interfaces.IAuthentication)
+


Property changes on: zc.objectlog/trunk/src/zc/objectlog/log.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Added: zc.objectlog/trunk/src/zc/objectlog/tests.py
===================================================================
--- zc.objectlog/trunk/src/zc/objectlog/tests.py	2006-08-15 21:08:39 UTC (rev 69540)
+++ zc.objectlog/trunk/src/zc/objectlog/tests.py	2006-08-15 21:11:43 UTC (rev 69541)
@@ -0,0 +1,87 @@
+##############################################################################
+#
+# Copyright (c) 2005 Zope Corporation. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Visible Source
+# License, Version 1.0 (ZVSL).  A copy of the ZVSL 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.
+#
+##############################################################################
+"""objectlog module test runner
+
+$Id: tests.py 12198 2006-06-14 20:56:25Z gary $
+"""
+
+import unittest, transaction
+from zope.testing import doctest, module
+from zope.testing.doctestunit import DocTestSuite
+from zope.component import testing
+
+def setUp(test):
+    testing.setUp(test)
+    module.setUp(test, 'zc.objectlog.log_txt')
+
+def tearDown(test):
+    module.tearDown(test)
+    testing.tearDown(test)
+    transaction.abort()
+    db = test.globs.get('db')
+    if db is not None:
+        db.close()
+
+def copierSetUp(test):
+    testing.setUp(test)
+    import zope.security.management
+    import zope.security.interfaces
+    import zope.app.security.interfaces
+    from zope import interface, schema
+    import zope.component
+    import zope.publisher.interfaces
+    class DummyPrincipal(object):
+        interface.implements(zope.security.interfaces.IPrincipal)
+        def __init__(self, id, title, description):
+            self.id = id
+            self.title = title
+            self.description = description
+    
+    alice = DummyPrincipal('alice', 'Alice Aal', 'a principled principal')
+    class DummyParticipation(object):
+        interface.implements(zope.publisher.interfaces.IRequest)
+        interaction = principal = None
+        def __init__(self, principal):
+            self.principal = principal
+    
+    class DummyAuthService(object):
+        interface.implements(zope.app.security.interfaces.IAuthentication)
+        def __init__(self, data):
+            self.data = data
+        def getPrincipal(self, id):
+            return self.data[id]
+    
+    auth = DummyAuthService({'alice': alice})
+    zope.component.provideUtility(auth)
+    zope.security.management.newInteraction(DummyParticipation(alice))
+    module.setUp(test, 'zc.objectlog.copier_txt')
+
+def copierTearDown(test):
+    import zope.security.management
+    zope.security.management.endInteraction()
+    module.tearDown(test)
+    testing.tearDown(test)
+
+def test_suite():
+    return unittest.TestSuite((
+        doctest.DocFileSuite('log.txt',
+                             setUp=setUp, tearDown=tearDown,),
+        doctest.DocFileSuite('copier.txt',
+                             setUp=copierSetUp, tearDown=copierTearDown,),
+        DocTestSuite('zc.objectlog.utils'),
+        ))
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')

Added: zc.objectlog/trunk/src/zc/objectlog/utils.py
===================================================================
--- zc.objectlog/trunk/src/zc/objectlog/utils.py	2006-08-15 21:08:39 UTC (rev 69540)
+++ zc.objectlog/trunk/src/zc/objectlog/utils.py	2006-08-15 21:11:43 UTC (rev 69541)
@@ -0,0 +1,127 @@
+##############################################################################
+#
+# Copyright (c) 2005 Zope Corporation. All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Visible Source
+# License, Version 1.0 (ZVSL).  A copy of the ZVSL 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.
+#
+##############################################################################
+"""utilities for the objectlog module
+
+$Id: utils.py 12198 2006-06-14 20:56:25Z gary $
+"""
+from zope import interface, schema
+import zope.interface.common.mapping
+from zope.schema.interfaces import RequiredMissing
+
+def validate(obj, i): # XXX put this in zope.schema?
+    i.validateInvariants(obj)
+    for name, field in schema.getFieldsInOrder(i):
+        value = field.query(obj, field.missing_value)
+        if value == field.missing_value:
+            if field.required:
+                raise RequiredMissing(name)
+        else:
+            bound = field.bind(obj)
+            bound.validate(value)
+
+# !!! The ImmutableDict class is from an unreleased ZPL package called aimles,
+# included for distribution here with the author's (Gary Poster) permission
+class ImmutableDict(dict):
+    """A dictionary that cannot be mutated (without resorting to superclass
+    tricks, as shown below).
+    
+      >>> d = ImmutableDict({'name':'Gary', 'age':33})
+      >>> d['name']
+      'Gary'
+      >>> d['age']
+      33
+      >>> d.get('foo')
+      >>> d.get('name')
+      'Gary'
+      >>> d['name'] = 'Karyn'
+      Traceback (most recent call last):
+      ...
+      RuntimeError: Immutable dictionary
+      >>> d.clear()
+      Traceback (most recent call last):
+      ...
+      RuntimeError: Immutable dictionary
+      >>> d.update({'answer':42})
+      Traceback (most recent call last):
+      ...
+      RuntimeError: Immutable dictionary
+      >>> del d['name']
+      Traceback (most recent call last):
+      ...
+      RuntimeError: Immutable dictionary
+      >>> d.setdefault('sense')
+      Traceback (most recent call last):
+      ...
+      RuntimeError: Immutable dictionary
+      >>> d.pop('name')
+      Traceback (most recent call last):
+      ...
+      RuntimeError: Immutable dictionary
+      >>> d.popitem()
+      Traceback (most recent call last):
+      ...
+      RuntimeError: Immutable dictionary
+      >>> d2 = ImmutableDict.fromkeys((1,2,3))
+      >>> type(d2.copy()) # copy is standard mutable dict
+      <type 'dict'>
+      >>> import pprint
+      >>> pprint.pprint(d2.copy()) # pprint gets confused by subtypes
+      {1: None, 2: None, 3: None}
+      >>> pprint.pprint(ImmutableDict.fromkeys((1,2,3),'foo'))
+      {1: 'foo', 2: 'foo', 3: 'foo'}
+    
+    Here's an example of actually mutating the dictionary anyway.
+    
+      >>> dict.__setitem__(d, 'age', 33*12 + 7)
+      >>> d['age']
+      403
+
+    pickling and unpickling is supported.
+    
+      >>> import pickle
+      >>> copy = pickle.loads(pickle.dumps(d))
+      >>> copy is d
+      False
+      >>> copy == d
+      True
+
+      >>> import cPickle
+      >>> copy = cPickle.loads(cPickle.dumps(d))
+      >>> copy is d
+      False
+      >>> copy == d
+      True
+    """
+    interface.implements(
+        zope.interface.common.mapping.IExtendedReadMapping,
+        zope.interface.common.mapping.IClonableMapping)
+    def __setitem__(self, key, val):
+        raise RuntimeError('Immutable dictionary')
+    def clear(self):
+        raise RuntimeError('Immutable dictionary')
+    def update(self, other):
+        raise RuntimeError('Immutable dictionary')
+    def __delitem__(self, key):
+        raise RuntimeError('Immutable dictionary')
+    def setdefault(self, key, failobj=None):
+        raise RuntimeError('Immutable dictionary')
+    def pop(self, key, *args):
+        raise RuntimeError('Immutable dictionary')
+    def popitem(self):
+        raise RuntimeError('Immutable dictionary')
+    @classmethod
+    def fromkeys(cls, iterable, value=None):
+        return cls(dict.fromkeys(iterable, value))
+



More information about the Checkins mailing list