-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

yuppie wrote:
> Hi Tres!
> 
> 
> Tres Seaver wrote:
> 
>> Jens Vagelpohl wrote:
>>
>>> On 15 Nov 2005, at 14:24, yuppie wrote:
>>>
>>>> The notes should be logged *and* used for reporting in the ZMI.
>>>>
>>>>
>>>> Implementation:
>>>>
>>>> I'm no logging expert, so I might well be missing something. The
>>>> state of the art seems to be using the Python logging package (PEP
>>>> 282). Is it possible to use that framework for reporting as well?  It
>>>> doesn't look like that.
>>>>
>>>> Replacing the 'note' method in ISetupContext with a more logger  like
>>>> API and sending the notes to the Python logger *and* to TTW  reports
>>>> might be the way to go.
>>>
>>>
>>> There could be a "multiplexer" that logs to the standard Zope event  log
>>> *and* keeps the messages in a memory buffer to be displayed in  the
>>> browser. This could be done in a separate class or a logging API  could
>>> be added to ISetupContext. Should be easy to do, really.
>>
>>
>> I *think* the current setup tool creates a text file with log messages
>> in it, and stores that file inside the tool.
> 
> 
> Couldn't find anything like that in the setup tool. It collects the
> messages returned by handlers, passes them around and forgets them after
> the request is finished. The _notes list of the setup context is ignored
> completely by the tool.

Hmm, it seems like that landed in the project-local repository from
which GenericSetup originally sprang, but *after* GenericSetup landed in
CMF's repository.  I'm attaching the patch:

  - It uses the '_notes' field both to create an OFS.Image.File log of
    import runs, as well as to display at the bottom of the "Import"
    tab.

  - It echoes logged messages to zLOG.

  - It expands ISetupContext's API with methods 'note', 'listNotes',
    and 'clearNotes'.  Stock implementations of ths API are in a new
    'SetupContextBase' class.

Note that the patch doesn't apply cleanly to the GenericSetup trunk;
I'll have to work out why if we chose to land it.

>> I would prefer to maintain
>> the data persistently, rather than in RAM;  the API for that could be
>> extended, of course.
> 
> 
> Why would you prefer persistent reports? Wouldn't it be sufficient to
> have the messages in the event log?

Having the log file present in the tool makes it possible to review what
happened without having filesystem access, which was importante for the
customer whose project inspired the non-CMF-specific GenericSetup.


Tres.
- --
===================================================================
Tres Seaver          +1 202-558-7113          [EMAIL PROTECTED]
Palladion Software   "Excellence by Design"    http://palladion.com
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.1 (GNU/Linux)
Comment: Using GnuPG with Thunderbird - http://enigmail.mozdev.org

iD8DBQFDeyi5+gerLs4ltQ4RAkmOAJ0YDoKjTsJWoYdfeNnmSFjjt5bngQCfRVVH
wt7dJmn0rRihR1EFZ5PVEV8=
=ym+y
-----END PGP SIGNATURE-----
Index: context.py
===================================================================
RCS file: /usr/local/cvsroot/Products/GenericSetup/context.py,v
retrieving revision 1.2
retrieving revision 1.5
diff -u -r1.2 -r1.5
--- context.py	11 Aug 2005 21:41:36 -0000	1.2
+++ context.py	23 Aug 2005 21:32:01 -0000	1.5
@@ -42,8 +42,50 @@
 from interfaces import IImportContext
 from permissions import ManagePortal
 
+class SetupContextBase( Implicit ):
 
-class DirectoryImportContext( Implicit ):
+    """ Base class for ISetupContext implementations.
+    """
+
+    security = ClassSecurityInfo()
+    def __init__(self, site):
+
+        self._site = site
+        self._messages = []
+
+    security.declareProtected( ManagePortal, 'getSite' )
+    def getSite(self):
+
+        """ See ISetupContext.
+        """
+        return aq_self(self._site)
+
+    security.declareProtected( ManagePortal, 'note' )
+    def note(self, component, message):
+
+        """ See ISetupContext.
+        """
+        import zLOG
+        zLOG.LOG('GenericSetup', zLOG.INFO, '%s: %s' % (component, message))
+        self._messages.append((component, message))
+
+    security.declareProtected( ManagePortal, 'listNotes' )
+    def listNotes(self):
+
+        """ See ISetupContext.
+        """
+        return self._messages[:]
+
+    security.declareProtected( ManagePortal, 'clearNotes' )
+    def clearNotes(self):
+
+        """ See ISetupContext.
+        """
+        self._messages[:] = []
+  
+InitializeClass(SetupContextBase)
+
+class DirectoryImportContext( SetupContextBase ):
 
     implements(IImportContext)
 
@@ -55,19 +97,11 @@
                 , should_purge=False
                 , encoding=None
                 ):
-
-        self._site = aq_parent( aq_inner( tool ) )
+        SetupContextBase.__init__( self, aq_parent( aq_inner( tool ) ) )
         self._profile_path = profile_path
         self._should_purge = bool( should_purge )
         self._encoding = encoding
 
-    security.declareProtected( ManagePortal, 'getSite' )
-    def getSite( self ):
-
-        """ See ISetupContext.
-        """
-        return aq_self(self._site)
-
     security.declareProtected( ManagePortal, 'getEncoding' )
     def getEncoding( self ):
 
@@ -145,7 +179,7 @@
 InitializeClass( DirectoryImportContext )
 
 
-class DirectoryExportContext( Implicit ):
+class DirectoryExportContext( SetupContextBase ):
 
     implements(IExportContext)
 
@@ -153,16 +187,9 @@
 
     def __init__( self, tool, profile_path ):
 
-        self._site = aq_parent( aq_inner( tool ) )
+        SetupContextBase.__init__( self, aq_parent( aq_inner( tool ) ) )
         self._profile_path = profile_path
 
-    security.declareProtected( ManagePortal, 'getSite' )
-    def getSite( self ):
-
-        """ See ISetupContext.
-        """
-        return aq_self(self._site)
-
     security.declareProtected( ManagePortal, 'writeDataFile' )
     def writeDataFile( self, filename, text, content_type, subdir=None ):
 
@@ -187,7 +214,7 @@
 InitializeClass( DirectoryExportContext )
 
 
-class TarballImportContext( Implicit ):
+class TarballImportContext( SetupContextBase ):
 
     implements(IImportContext)
 
@@ -195,7 +222,7 @@
 
     def __init__( self, tool, archive_bits, encoding=None, should_purge=False ):
 
-        self._site = aq_parent( aq_inner( tool ) )
+        SetupContextBase.__init__( self, aq_parent( aq_inner( tool ) ) )
         timestamp = time.gmtime()
         self._archive_stream = StringIO(archive_bits)
         self._archive = TarFile.open( 'foo.bar', 'r:gz'
@@ -203,13 +230,6 @@
         self._encoding = encoding
         self._should_purge = bool( should_purge )
 
-    security.declareProtected( ManagePortal, 'getSite' )
-    def getSite( self ):
-
-        """ See ISetupContext.
-        """
-        return aq_self(self._site)
-
     def getEncoding( self ):
 
         """ See IImportContext.
@@ -285,7 +305,7 @@
             return None
 
 
-class TarballExportContext( Implicit ):
+class TarballExportContext( SetupContextBase ):
 
     implements(IExportContext)
 
@@ -293,7 +313,7 @@
 
     def __init__( self, tool ):
 
-        self._site = aq_parent( aq_inner( tool ) )
+        SetupContextBase.__init__( self, aq_parent( aq_inner( tool ) ) )
         timestamp = time.gmtime()
         archive_name = ( 'setup_tool-%4d%02d%02d%02d%02d%02d.tar.gz'
                        % timestamp[:6] )
@@ -303,13 +323,6 @@
         self._archive = TarFile.open( archive_name, 'w:gz'
                                     , self._archive_stream )
 
-    security.declareProtected( ManagePortal, 'getSite' )
-    def getSite( self ):
-
-        """ See ISetupContext.
-        """
-        return aq_self(self._site)
-
     security.declareProtected( ManagePortal, 'writeDataFile' )
     def writeDataFile( self, filename, text, content_type, subdir=None ):
 
@@ -342,7 +355,7 @@
 InitializeClass( TarballExportContext )
 
 
-class SnapshotExportContext( Implicit ):
+class SnapshotExportContext( SetupContextBase ):
 
     implements(IExportContext)
 
@@ -351,16 +364,9 @@
     def __init__( self, tool, snapshot_id ):
 
         self._tool = tool = aq_inner( tool )
-        self._site = aq_parent( tool )
+        SetupContextBase.__init__( self, aq_parent( tool ) )
         self._snapshot_id = snapshot_id
 
-    security.declareProtected( ManagePortal, 'getSite' )
-    def getSite( self ):
-
-        """ See ISetupContext.
-        """
-        return aq_self(self._site)
-
     security.declareProtected( ManagePortal, 'writeDataFile' )
     def writeDataFile( self, filename, text, content_type, subdir=None ):
 
@@ -441,7 +447,7 @@
 InitializeClass( SnapshotExportContext )
 
 
-class SnapshotImportContext( Implicit ):
+class SnapshotImportContext( SetupContextBase ):
 
     implements(IImportContext)
 
@@ -455,17 +461,10 @@
                 ):
 
         self._tool = tool = aq_inner( tool )
-        self._site = aq_parent( tool )
+        SetupContextBase.__init__( self, aq_parent( tool ) )
         self._snapshot_id = snapshot_id
         self._encoding = encoding
         self._should_purge = bool( should_purge )
-
-    security.declareProtected( ManagePortal, 'getSite' )
-    def getSite( self ):
-
-        """ See ISetupContext.
-        """
-        return aq_self(self._site)
 
     security.declareProtected( ManagePortal, 'getEncoding' )
     def getEncoding( self ):
Index: interfaces.py
===================================================================
RCS file: /usr/local/cvsroot/Products/GenericSetup/interfaces.py,v
retrieving revision 1.1.1.1
retrieving revision 1.3
diff -u -r1.1.1.1 -r1.3
--- interfaces.py	8 Aug 2005 19:38:37 -0000	1.1.1.1
+++ interfaces.py	23 Aug 2005 19:54:58 -0000	1.3
@@ -35,6 +35,26 @@
         """ Return the site object being configured / dumped.
         """
 
+    def note(component, message):
+
+        """ Record a message about the state of the operation.
+
+        o 'component' is a string identifying the subcomponent recording
+          the message.
+
+        o 'message' is the message text.
+        """
+
+    def listNotes():
+        """ Return notes recorded by this context.
+        
+        o Result a sequence of (component, message) tuples
+        """
+
+    def clearNotes():
+        """ Clear all notes recorded by this context.
+        """
+
 class IImportContext( ISetupContext ):
 
     def getEncoding():
Index: tool.py
===================================================================
RCS file: /usr/local/cvsroot/Products/GenericSetup/tool.py,v
retrieving revision 1.1.1.1
retrieving revision 1.3
diff -u -r1.1.1.1 -r1.3
--- tool.py	8 Aug 2005 19:38:37 -0000	1.1.1.1
+++ tool.py	25 Aug 2005 20:36:52 -0000	1.3
@@ -23,6 +23,7 @@
 from Acquisition import aq_base
 from Globals import InitializeClass
 from OFS.Folder import Folder
+from OFS.Image import File
 from Products.PageTemplates.PageTemplateFile import PageTemplateFile
 from zope.interface import implements
 from zope.interface import implementedBy
@@ -216,7 +217,9 @@
                     steps.append(dependency)
 
         message = self._doRunImportStep(step_id, context)
-        messages[step_id] = message
+        message_list = filter(None, [message])
+        message_list.extend( ['%s: %s' % x for x in context.listNotes()] )
+        messages[step_id] = '\n'.join(message_list)
         steps.append(step_id)
 
         return { 'steps' : steps, 'messages' : messages }
@@ -233,7 +236,10 @@
 
         for step in steps:
             message = self._doRunImportStep(step, context)
-            messages[step] = message
+            message_list = filter(None, [message])
+            message_list.extend( ['%s: %s' % x for x in context.listNotes()] )
+            messages[step] = '\n'.join(message_list)
+            context.clearNotes()
 
         return { 'steps' : steps, 'messages' : messages }
 
@@ -386,33 +392,44 @@
                                    ids,
                                    run_dependencies,
                                    RESPONSE,
+                                   create_report=True,
                                   ):
         """ Import the steps selected by the user.
         """
         if not ids:
-            message = 'No+steps+selected.'
+            summary = 'No+steps+selected.'
 
         else:
             steps_run = []
+            messages = {}
             for step_id in ids:
                 result = self.runImportStep(step_id, run_dependencies)
                 steps_run.extend(result['steps'])
+                messages.update(result['messages'])
 
-            message = 'Steps+run:%s' % '+,'.join(steps_run)
+            summary = 'Steps+run:%s' % '+,'.join(steps_run)
 
-        RESPONSE.redirect('%s/manage_importSteps?manage_tabs_message=%s'
-                         % (self.absolute_url(), message))
+        if create_report:
+            name = self._mangleTimestampName('import-selected', 'log')
+            self._createReport(name, result['steps'], result['messages'])
+
+        return self.manage_importSteps(manage_tabs_message=summary,
+                                       messages=messages)
 
     security.declareProtected(ManagePortal, 'manage_importSelectedSteps')
-    def manage_importAllSteps(self, RESPONSE):
+    def manage_importAllSteps(self, RESPONSE, create_report=True):
 
         """ Import all steps.
         """
         result = self.runAllImportSteps()
-        message = 'Steps+run:%s' % '+,'.join(result['steps'])
+        steps_run = 'Steps+run:%s' % '+,'.join(result['steps'])
+
+        if create_report:
+            name = self._mangleTimestampName('import-all', 'log')
+            self._createReport(name, result['steps'], result['messages'])
 
-        RESPONSE.redirect('%s/manage_importSteps?manage_tabs_message=%s'
-                         % (self.absolute_url(), message))
+        return self.manage_importSteps(manage_tabs_message=steps_run,
+                                       messages=result['messages'])
 
     security.declareProtected(ManagePortal, 'manage_exportSteps')
     manage_exportSteps = PageTemplateFile('sutExportSteps', _wwwdir)
@@ -514,8 +531,7 @@
         o If no ID is passed, generate one.
         """
         if snapshot_id is None:
-            timestamp = time.gmtime()
-            snapshot_id = 'snapshot-%4d%02d%02d%02d%02d%02d' % timestamp[:6]
+            snapshot_id = self._mangleTimestampName('snapshot')
 
         self.createSnapshot(snapshot_id)
 
@@ -709,6 +725,48 @@
                , 'tarball' : context.getArchive()
                , 'filename' : context.getArchiveFilename()
                }
+
+    security.declarePrivate('_mangleTimestampName')
+    def _mangleTimestampName(self, prefix, ext=None):
+
+        """ Create a mangled ID using a timestamp.
+        """
+        timestamp = time.gmtime()
+        items = (prefix,) + timestamp[:6]
+
+        if ext is None:
+            fmt = '%s-%4d%02d%02d%02d%02d%02d'
+        else:
+            fmt = '%s-%4d%02d%02d%02d%02d%02d.%s'
+            items += (ext,)
+
+        return fmt % items
+
+    security.declarePrivate('_createReport')
+    def _createReport(self, name, steps, messages):
+
+        """ Record the results of a run.
+        """
+        lines = []
+        # Create report
+        for step in steps:
+            lines.append('=' * 65)
+            lines.append('Step: %s' % step)
+            lines.append('=' * 65)
+            msg = messages[step]
+            lines.extend(msg.split('\n'))
+            lines.append('')
+
+        report = '\n'.join(lines)
+        if isinstance(report, unicode):
+            report = report.encode('latin-1')
+
+        file = File(id=name,
+                    title='',
+                    file=report,
+                    content_type='text/plain'
+                   )
+        self._setObject(name, file)
 
 InitializeClass(SetupTool)
 
Index: version.txt
===================================================================
RCS file: /usr/local/cvsroot/Products/GenericSetup/version.txt,v
retrieving revision 1.2
retrieving revision 1.6
diff -u -r1.2 -r1.6
--- version.txt	11 Aug 2005 21:41:36 -0000	1.2
+++ version.txt	29 Aug 2005 16:26:33 -0000	1.6
@@ -1 +1 @@
-GenericSetup-0.10
+GenericSetup-0.12
Index: tests/common.py
===================================================================
RCS file: /usr/local/cvsroot/Products/GenericSetup/tests/common.py,v
retrieving revision 1.1.1.1
retrieving revision 1.2
diff -u -r1.1.1.1 -r1.2
--- tests/common.py	8 Aug 2005 19:38:37 -0000	1.1.1.1
+++ tests/common.py	23 Aug 2005 16:45:21 -0000	1.2
@@ -179,10 +179,14 @@
     def __init__( self, site ):
         self._site = site
         self._wrote = []
+        self._notes = []
 
     def getSite( self ):
         return self._site
 
+    def note( self, component, message ):
+        self._notes.append((component, message))
+
     def writeDataFile( self, filename, text, content_type, subdir=None ):
         if subdir is not None:
             filename = '%s/%s' % ( subdir, filename )
@@ -195,9 +199,13 @@
         self._purge = purge
         self._encoding = encoding
         self._files = {}
+        self._notes = []
 
     def getSite( self ):
         return self._site
+
+    def note( self, component, message ):
+        self._notes.append((component, message))
 
     def getEncoding( self ):
         return self._encoding
Index: tests/test_context.py
===================================================================
RCS file: /usr/local/cvsroot/Products/GenericSetup/tests/test_context.py,v
retrieving revision 1.2
retrieving revision 1.4
diff -u -r1.2 -r1.4
--- tests/test_context.py	11 Aug 2005 21:41:36 -0000	1.2
+++ tests/test_context.py	23 Aug 2005 19:54:58 -0000	1.4
@@ -52,7 +52,7 @@
 class DummyTool( Folder ):
 
     pass
-
+        
 class DirectoryImportContextTests( FilesystemTestBase
                                  , ConformsToISetupContext
                                  , ConformsToIImportContext
@@ -65,6 +65,22 @@
         from Products.GenericSetup.context import DirectoryImportContext
         return DirectoryImportContext
 
+    def test_note( self ):
+
+        site = DummySite( 'site' ).__of__( self.root )
+        ctx = self._makeOne( site, self._PROFILE_PATH )
+        self.assertEqual( len( ctx.listNotes() ), 0 )
+
+        ctx.note( 'foo', 'bar' )
+
+        self.assertEqual( len( ctx.listNotes() ), 1 )
+        component, message = ctx.listNotes()[0]
+        self.assertEqual( component, 'foo' )
+        self.assertEqual( message, 'bar' )
+
+        ctx.clearNotes()
+        self.assertEqual( len( ctx.listNotes() ), 0 )
+
     def test_readDataFile_nonesuch( self ):
 
         FILENAME = 'nonesuch.txt'
@@ -332,6 +348,22 @@
         from Products.GenericSetup.context import DirectoryExportContext
         return DirectoryExportContext
 
+    def test_note( self ):
+
+        site = DummySite( 'site' ).__of__( self.root )
+        ctx = self._makeOne( site, self._PROFILE_PATH )
+        self.assertEqual( len( ctx.listNotes() ), 0 )
+
+        ctx.note( 'foo', 'bar' )
+
+        self.assertEqual( len( ctx.listNotes() ), 1 )
+        component, message = ctx.listNotes()[0]
+        self.assertEqual( component, 'foo' )
+        self.assertEqual( message, 'bar' )
+
+        ctx.clearNotes()
+        self.assertEqual( len( ctx.listNotes() ), 0 )
+
     def test_writeDataFile_simple( self ):
 
         from string import printable, digits
@@ -444,6 +476,21 @@
 
         return site, tool, ctx.__of__( tool )
 
+    def test_note( self ):
+
+        site, tool, ctx = self._makeOne()
+        self.assertEqual( len( ctx.listNotes() ), 0 )
+
+        ctx.note( 'foo', 'bar' )
+
+        self.assertEqual( len( ctx.listNotes() ), 1 )
+        component, message = ctx.listNotes()[0]
+        self.assertEqual( component, 'foo' )
+        self.assertEqual( message, 'bar' )
+
+        ctx.clearNotes()
+        self.assertEqual( len( ctx.listNotes() ), 0 )
+
     def test_ctorparms( self ):
 
         ENCODING = 'latin-1'
@@ -696,6 +743,23 @@
         from Products.GenericSetup.context import TarballExportContext
         return TarballExportContext
 
+    def test_note( self ):
+
+        site = DummySite( 'site' ).__of__( self.root )
+        ctx = self._getTargetClass()( site )
+
+        self.assertEqual( len( ctx.listNotes() ), 0 )
+
+        ctx.note( 'foo', 'bar' )
+
+        self.assertEqual( len( ctx.listNotes() ), 1 )
+        component, message = ctx.listNotes()[0]
+        self.assertEqual( component, 'foo' )
+        self.assertEqual( message, 'bar' )
+
+        ctx.clearNotes()
+        self.assertEqual( len( ctx.listNotes() ), 0 )
+
     def test_writeDataFile_simple( self ):
 
         from string import printable
@@ -760,6 +824,25 @@
 
         return self._getTargetClass()( *args, **kw )
 
+    def test_note( self ):
+
+        site = DummySite( 'site' ).__of__( self.root )
+        site.setup_tool = DummyTool( 'setup_tool' )
+        tool = site.setup_tool
+        ctx = self._makeOne( tool, 'simple' )
+
+        self.assertEqual( len( ctx.listNotes() ), 0 )
+
+        ctx.note( 'foo', 'bar' )
+
+        self.assertEqual( len( ctx.listNotes() ), 1 )
+        component, message = ctx.listNotes()[0]
+        self.assertEqual( component, 'foo' )
+        self.assertEqual( message, 'bar' )
+
+        ctx.clearNotes()
+        self.assertEqual( len( ctx.listNotes() ), 0 )
+
     def test_writeDataFile_simple_image( self ):
 
         from OFS.Image import Image
@@ -1001,6 +1084,23 @@
             file.bobobase_modification_time = __faux_mod_time
 
         return folder._getOb( filename )
+
+    def test_note( self ):
+
+        SNAPSHOT_ID = 'note'
+        site, tool, ctx = self._makeOne( SNAPSHOT_ID )
+
+        self.assertEqual( len( ctx.listNotes() ), 0 )
+
+        ctx.note( 'foo', 'bar' )
+
+        self.assertEqual( len( ctx.listNotes() ), 1 )
+        component, message = ctx.listNotes()[0]
+        self.assertEqual( component, 'foo' )
+        self.assertEqual( message, 'bar' )
+
+        ctx.clearNotes()
+        self.assertEqual( len( ctx.listNotes() ), 0 )
 
     def test_ctorparms( self ):
 
Index: www/sutImportSteps.zpt
===================================================================
RCS file: /usr/local/cvsroot/Products/GenericSetup/www/sutImportSteps.zpt,v
retrieving revision 1.1.1.1
retrieving revision 1.2
diff -u -r1.1.1.1 -r1.2
--- www/sutImportSteps.zpt	8 Aug 2005 19:38:37 -0000	1.1.1.1
+++ www/sutImportSteps.zpt	23 Aug 2005 19:54:58 -0000	1.2
@@ -73,6 +73,19 @@
   </tr>
  </tbody>
 
+ <tbody tal:condition="options/messages | nothing">
+  <tr class="list-header">
+   <td colspan="4">Message Log</td>
+  </tr>
+  <tr valign="top"
+      tal:repeat="item options/messages/items">
+   <td tal:content="python: item[0]">STEP</td>
+   <td colspan="3"
+       tal:content="structure python: item[1].replace('\n', '<br />')"
+       >MESSAGE</td>
+  </tr>
+ </tbody>
+
 </table>
 </form>
 
_______________________________________________
Zope-CMF maillist  -  Zope-CMF@lists.zope.org
http://mail.zope.org/mailman/listinfo/zope-cmf

See http://collector.zope.org/CMF for bug reports and feature requests

Reply via email to