Commits

Olemis Lang  committed 4ec8b66

Trac #11148 : 'IAttachmentChangeListener' notifications powered by 'ListenerNotifier.notify' [ok]

  • Participants
  • Parent commits e4d8cbb
  • Branches trac_t11148

Comments (0)

Files changed (3)

 t11148/t11148_r11782_IEntityChangeListener_v2.diff
+t11148/t11148_r11784_IEntityListener_compat_attachment.diff
 #t11148/t11148_r11767_IResourceChangeListener.diff

File t11148/t11148_r11782_IEntityChangeListener_v2.diff

 
 diff -r 8d0c3223a818 trac/core.py
 --- a/trac/core.py	Thu Apr 18 14:30:21 2013 +0000
-+++ b/trac/core.py	Thu Apr 25 23:25:56 2013 -0500
++++ b/trac/core.py	Fri Apr 26 00:27:26 2013 -0500
 @@ -165,7 +165,6 @@
  
          locals_.setdefault('_implements', []).extend(interfaces)
  implements = Component.implements
  
  
-@@ -236,3 +235,99 @@
+@@ -236,3 +235,107 @@
          with the given class will not be available.
          """
          return True
 +    BUILTIN_EVENTS = set(['created', 'changed', 'deleted', 'reparented'])
 +
 +    def notify(self, method, entity, **kwargs):
++        from trac.util.text import exception_to_unicode
++
 +        interface = method.im_class
 +        method_name = method.__name__
 +        if interface is None:
 +        xp = ExtensionPoint(interface)
 +        # FIXME : Sender component rather than self. Change method signature
 +        for listener in xp.extensions(self):
-+            getattr(listener, method_name)(entity, **kwargs)
++            try:
++                _method = getattr(listener, method_name)
++            except AttributeError:
++                self.log.warning("Event listener %s doesn't support method %s",
++                                 listener.__class__.__name__, method_name)
++            else:
++                _method(entity, **kwargs)
 +
 +        xp = ExtensionPoint(IEntityChangeListener)
 +        generic_listeners = xp.extensions(self)
 +
 +            for listener in generic_listeners:
 +                getattr(listener, method_name)(**kwargs)
+diff -r 8d0c3223a818 trac/tests/core.py
+--- a/trac/tests/core.py	Thu Apr 18 14:30:21 2013 +0000
++++ b/trac/tests/core.py	Fri Apr 26 00:27:26 2013 -0500
+@@ -15,7 +15,7 @@
+ # Author: Christopher Lenz <cmlenz@gmx.de>
+ 
+ from trac.core import *
+-from trac.core import ComponentManager
++from trac.core import ComponentManager, IEntityChangeListener
+ 
+ import unittest
+ 
+@@ -341,6 +341,56 @@
+         self.assertEqual(None, mgr[ComponentA])
+ 
+ 
++class GenericEntitiesChangeListenerMock(Component):
++    """Utility class to test generic event listeners 
++    """
++    implements(IEntityChangeListener)
++
++    def __init__(self):
++        self.details = []
++        self.action = []
++
++    def _handle(self, kwargs):
++        # Add action for custom events
++        changeinfo = kwargs.get('changeinfo')
++        if changeinfo is not None and changeinfo.action:
++            kwargs['action'] = changeinfo.action
++        # Add custom args
++        kwargs.update(changeinfo.event_args or {})
++
++        # Remove reference to self and clear changeinfo
++        del kwargs['self']
++        del kwargs['changeinfo']
++        self.details.append(kwargs)
++
++        cls = kwargs['entity'].__class__
++        self.action.append(cls.__name__.lower() + '_' + kwargs['action'])
++
++    def entity_created(self, entity, changeinfo=None):
++        kwargs = locals()
++        kwargs.update(action='created')
++        self._handle(kwargs)
++
++    def entity_changed(self, entity, old_values, changeinfo = None):
++        kwargs = locals()
++        kwargs.update(action='changed')
++        self._handle(kwargs)
++
++    def entity_deleted(self, entity, changeinfo = None):
++        kwargs = locals()
++        kwargs.update(action='deleted')
++        self._handle(kwargs)
++
++    def entity_reparented(self, entity, changeinfo = None):
++        kwargs = locals()
++        kwargs.update(action='reparented')
++        self._handle(kwargs)
++
++    def entity_event(self, entity, changeinfo = None):
++        kwargs = locals()
++        self._handle(kwargs)
++
++
+ def suite():
+     return unittest.makeSuite(ComponentTestCase, 'test')
+ 
 diff -r 8d0c3223a818 trac/ticket/model.py
 --- a/trac/ticket/model.py	Thu Apr 18 14:30:21 2013 +0000
-+++ b/trac/ticket/model.py	Thu Apr 25 23:25:56 2013 -0500
++++ b/trac/ticket/model.py	Fri Apr 26 00:27:26 2013 -0500
 @@ -25,7 +25,8 @@
  from trac.attachment import Attachment
  from trac import core
          """
 diff -r 8d0c3223a818 trac/ticket/tests/model.py
 --- a/trac/ticket/tests/model.py	Thu Apr 18 14:30:21 2013 +0000
-+++ b/trac/ticket/tests/model.py	Thu Apr 25 23:25:56 2013 -0500
-@@ -9,11 +9,11 @@
++++ b/trac/ticket/tests/model.py	Fri Apr 26 00:27:26 2013 -0500
+@@ -9,15 +9,16 @@
  
  from trac import core
  from trac.attachment import Attachment
  from trac.ticket.api import (
      IMilestoneChangeListener, ITicketChangeListener, TicketSystem
  )
-@@ -1097,6 +1097,416 @@
+ from trac.test import EnvironmentStub
++from trac.tests.core import GenericEntitiesChangeListenerMock
+ from trac.util.datefmt import from_utimestamp, to_utimestamp, utc
+ 
+ 
+@@ -1097,6 +1098,368 @@
          self.assertEqual([('Test', 0, 'Some text')], self.env.db_query(
              "SELECT name, time, description FROM version WHERE name='Test'"))
  
 +        return entity
 +
 +
-+class GenericEntitiesChangeListenerMock(core.Component):
-+    implements(core.IEntityChangeListener)
-+
-+    def __init__(self):
-+        self.details = []
-+        self.action = []
-+
-+    def _handle(self, kwargs):
-+        # Add action for custom events
-+        changeinfo = kwargs.get('changeinfo')
-+        if changeinfo is not None and changeinfo.action:
-+            kwargs['action'] = changeinfo.action
-+        # Add custom args
-+        kwargs.update(changeinfo.event_args or {})
-+
-+        # Remove reference to self and clear changeinfo
-+        del kwargs['self']
-+        del kwargs['changeinfo']
-+        self.details.append(kwargs)
-+
-+        cls = kwargs['entity'].__class__
-+        self.action.append(cls.__name__.lower() + '_' + kwargs['action'])
-+
-+    def entity_created(self, entity, changeinfo=None):
-+        kwargs = locals()
-+        kwargs.update(action='created')
-+        self._handle(kwargs)
-+
-+    def entity_changed(self, entity, old_values, changeinfo = None):
-+        kwargs = locals()
-+        kwargs.update(action='changed')
-+        self._handle(kwargs)
-+
-+    def entity_deleted(self, entity, changeinfo = None):
-+        kwargs = locals()
-+        kwargs.update(action='deleted')
-+        self._handle(kwargs)
-+
-+    def entity_reparented(self, entity, changeinfo = None):
-+        kwargs = locals()
-+        kwargs.update(action='reparented')
-+        self._handle(kwargs)
-+
-+    def entity_event(self, entity, changeinfo = None):
-+        kwargs = locals()
-+        self._handle(kwargs)
-+
-+
 +class GenericEntitiesChangeListenerTestCase(MultipleEntitiesChangeListenerTestCase):
 +
 +    def setUp(self):
  
  def suite():
      suite = unittest.TestSuite()
-@@ -1107,6 +1517,17 @@
+@@ -1107,6 +1470,17 @@
      suite.addTest(unittest.makeSuite(MilestoneTestCase, 'test'))
      suite.addTest(unittest.makeSuite(ComponentTestCase, 'test'))
      suite.addTest(unittest.makeSuite(VersionTestCase, 'test'))

File t11148/t11148_r11784_IEntityListener_compat_attachment.diff

+# HG changeset patch
+# Parent e01d7454b5fe56724d7abd0c80143e78aa8deb77
+Trac #11148 : 'IAttachmentListener' notifications powered by 'ListenerNotifier.notify'
+
+diff -r e01d7454b5fe trac/attachment.py
+--- a/trac/attachment.py	Fri Apr 26 00:27:27 2013 -0500
++++ b/trac/attachment.py	Fri Apr 26 00:59:58 2013 -0500
+@@ -34,6 +34,7 @@
+                        console_datetime_format, get_dir_list
+ from trac.config import BoolOption, IntOption
+ from trac.core import *
++from trac.core import ListenerNotifier
+ from trac.mimeview import *
+ from trac.perm import PermissionError, IPermissionPolicy
+ from trac.resource import *
+@@ -235,8 +236,8 @@
+ 
+         self.env.log.info("Attachment removed: %s" % self.title)
+ 
+-        for listener in AttachmentModule(self.env).change_listeners:
+-            listener.attachment_deleted(self)
++        ListenerNotifier(self.env).notify(
++                     IAttachmentChangeListener.attachment_deleted, entity=self)
+ 
+     def reparent(self, new_realm, new_id):
+         assert self.filename, "Cannot reparent non-existent attachment"
+@@ -284,9 +285,10 @@
+ 
+         self.env.log.info("Attachment reparented: %s" % self.title)
+ 
+-        for listener in AttachmentModule(self.env).change_listeners:
+-            if hasattr(listener, 'attachment_reparented'):
+-                listener.attachment_reparented(self, old_realm, old_id)
++        ListenerNotifier(self.env).notify(
++                     IAttachmentChangeListener.attachment_reparented,
++                     entity=self, old_parent_realm=old_realm,
++                     old_parent_id=old_id)
+ 
+     def insert(self, filename, fileobj, size, t=None, db=None):
+         """Create a new Attachment record and save the file content.
+@@ -330,8 +332,8 @@
+                 self.env.log.info("New attachment: %s by %s", self.title,
+                                   self.author)
+ 
+-        for listener in AttachmentModule(self.env).change_listeners:
+-            listener.attachment_added(self)
++        ListenerNotifier(self.env).notify(
++                     IAttachmentChangeListener.attachment_added, entity=self)
+ 
+ 
+     @classmethod
+diff -r e01d7454b5fe trac/tests/attachment.py
+--- a/trac/tests/attachment.py	Fri Apr 26 00:27:27 2013 -0500
++++ b/trac/tests/attachment.py	Fri Apr 26 00:59:58 2013 -0500
+@@ -6,11 +6,13 @@
+ import tempfile
+ import unittest
+ 
+-from trac.attachment import Attachment, AttachmentModule
++from trac.attachment import Attachment, AttachmentModule, \
++                            IAttachmentChangeListener
+ from trac.core import Component, implements, TracError
+ from trac.perm import IPermissionPolicy, PermissionCache
+ from trac.resource import Resource, resource_exists
+ from trac.test import EnvironmentStub
++from trac.tests.core import GenericEntitiesChangeListenerMock 
+ 
+ 
+ hashes = {
+@@ -39,6 +41,26 @@
+             return None
+ 
+ 
++class TestAttachmentListener(Component):
++    implements(IAttachmentChangeListener)
++
++    def __init__(self):
++        self.details = []
++
++    def attachment_added(self, attachment):
++        self.details.append(dict(action='added', entity=attachment))
++
++    def attachment_deleted(self, attachment):
++        self.details.append(dict(action='deleted', entity=attachment))
++
++    def attachment_reparented(self, attachment, old_parent_realm, old_parent_id):
++        kwargs = locals()
++        kwargs.pop('self')
++        kwargs['entity'] = kwargs.pop('attachment')
++        kwargs['action'] = 'reparented'
++        self.details.append(kwargs)
++
++
+ class AttachmentTestCase(unittest.TestCase):
+ 
+     def setUp(self):
+@@ -52,6 +74,8 @@
+         self.env.config.set('attachment', 'max_size', 512)
+ 
+         self.perm = PermissionCache(self.env)
++        self.attachment_listener = TestAttachmentListener(self.env)
++        self.entity_listener = GenericEntitiesChangeListenerMock(self.env)
+ 
+     def tearDown(self):
+         shutil.rmtree(self.env.path)
+@@ -122,28 +146,42 @@
+                           Attachment.select(self.env, 'wiki', 'SomePage').next)
+ 
+     def test_insert(self):
+-        attachment = Attachment(self.env, 'ticket', 42)
+-        attachment.insert('foo.txt', StringIO(''), 0, 1)
+-        attachment = Attachment(self.env, 'ticket', 42)
+-        attachment.insert('bar.jpg', StringIO(''), 0, 2)
++        attachment1 = Attachment(self.env, 'ticket', 42)
++        attachment1.insert('foo.txt', StringIO(''), 0, 1)
++        attachment2 = Attachment(self.env, 'ticket', 42)
++        attachment2.insert('bar.jpg', StringIO(''), 0, 2)
+ 
+         attachments = Attachment.select(self.env, 'ticket', 42)
+         self.assertEqual('foo.txt', attachments.next().filename)
+         self.assertEqual('bar.jpg', attachments.next().filename)
+         self.assertRaises(StopIteration, attachments.next)
+ 
++        expected_events = [{'action' : 'added', 'entity' : attachment1},
++                           {'action' : 'added', 'entity' : attachment2}]
++        self.assertEquals(expected_events, self.attachment_listener.details,
++                          "IAttachmentChangeListener event sequence")
++        self.assertEquals(expected_events, self.entity_listener.details,
++                          "IEntityChangeListener event sequence")
++
+     def test_insert_unique(self):
+-        attachment = Attachment(self.env, 'ticket', 42)
+-        attachment.insert('foo.txt', StringIO(''), 0)
+-        self.assertEqual('foo.txt', attachment.filename)
+-        attachment = Attachment(self.env, 'ticket', 42)
+-        attachment.insert('foo.txt', StringIO(''), 0)
+-        self.assertEqual('foo.2.txt', attachment.filename)
++        attachment1 = Attachment(self.env, 'ticket', 42)
++        attachment1.insert('foo.txt', StringIO(''), 0)
++        self.assertEqual('foo.txt', attachment1.filename)
++        attachment2 = Attachment(self.env, 'ticket', 42)
++        attachment2.insert('foo.txt', StringIO(''), 0)
++        self.assertEqual('foo.2.txt', attachment2.filename)
+         self.assertEqual(os.path.join(self.attachments_dir, 'ticket',
+                                       hashes['42'][0:3], hashes['42'],
+                                       hashes['foo.2.txt'] + '.txt'),
+-                         attachment.path)
+-        self.assert_(os.path.exists(attachment.path))
++                         attachment2.path)
++        self.assert_(os.path.exists(attachment2.path))
++
++        expected_events = [{'action' : 'added', 'entity' : attachment1},
++                           {'action' : 'added', 'entity' : attachment2}]
++        self.assertEquals(expected_events, self.attachment_listener.details,
++                          "IAttachmentChangeListener event sequence")
++        self.assertEquals(expected_events, self.entity_listener.details,
++                          "IEntityChangeListener event sequence")
+ 
+     def test_insert_outside_attachments_dir(self):
+         attachment = Attachment(self.env, '../../../../../sth/private', 42)
+@@ -168,6 +206,15 @@
+         attachments = Attachment.select(self.env, 'wiki', 'SomePage')
+         self.assertEqual(0, len(list(attachments)))
+ 
++        expected_events = [{'action' : 'added', 'entity' : attachment1},
++                           {'action' : 'added', 'entity' : attachment2},
++                           {'action' : 'deleted', 'entity' : attachment1},
++                           {'action' : 'deleted', 'entity' : attachment2}]
++        self.assertEquals(expected_events, self.attachment_listener.details,
++                          "IAttachmentChangeListener event sequence")
++        self.assertEquals(expected_events, self.entity_listener.details,
++                          "IEntityChangeListener event sequence")
++
+     def test_delete_file_gone(self):
+         """
+         Verify that deleting an attachment works even if the referenced file
+@@ -179,6 +226,13 @@
+ 
+         attachment.delete()
+ 
++        expected_events = [{'action' : 'added', 'entity' : attachment},
++                           {'action' : 'deleted', 'entity' : attachment}]
++        self.assertEquals(expected_events, self.attachment_listener.details,
++                          "IAttachmentChangeListener event sequence")
++        self.assertEquals(expected_events, self.entity_listener.details,
++                          "IEntityChangeListener event sequence")
++
+     def test_reparent(self):
+         attachment1 = Attachment(self.env, 'wiki', 'SomePage')
+         attachment1.insert('foo.txt', StringIO(''), 0)
+@@ -205,6 +259,17 @@
+         assert not os.path.exists(path1) and os.path.exists(attachment1.path)
+         assert os.path.exists(attachment2.path)
+ 
++        expected_events = [{'action' : 'added', 'entity' : attachment1},
++                           {'action' : 'added', 'entity' : attachment2},
++                           {'action' : 'reparented', 'entity' : attachment1,
++                            'old_parent_realm' : 'wiki', 
++                            'old_parent_id' : 'SomePage'}]
++        self.assertEquals(expected_events, self.attachment_listener.details,
++                          "IAttachmentChangeListener event sequence")
++        self.assertEquals(expected_events, self.entity_listener.details,
++                          "IEntityChangeListener event sequence")
++
++
+     def test_legacy_permission_on_parent(self):
+         """Ensure that legacy action tests are done on parent.  As
+         `ATTACHMENT_VIEW` maps to `TICKET_VIEW`, the `TICKET_VIEW` is tested
+@@ -221,6 +286,12 @@
+         att.insert('file.txt', StringIO(''), 1)
+         self.assertTrue(resource_exists(self.env, att.resource))
+ 
++        expected_events = [{'action' : 'added', 'entity' : att}]
++        self.assertEquals(expected_events, self.attachment_listener.details,
++                          "IAttachmentChangeListener event sequence")
++        self.assertEquals(expected_events, self.entity_listener.details,
++                          "IEntityChangeListener event sequence")
++
+ 
+ def suite():
+     suite = unittest.TestSuite()