[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