Commits

Olemis Lang committed ceab031

Trac #11148 : Remove IResourceChangeListener patch

  • Participants
  • Parent commits 4ec8b66
  • Branches trac_t11148

Comments (0)

Files changed (1)

File t11148/t11148_r11767_IResourceChangeListener.diff

-Index: trac/resource.py
-===================================================================
---- trac/resource.py	(revision 11766)
-+++ trac/resource.py	(working copy)
-@@ -15,6 +15,7 @@
- #
- # Author: Christian Boos <cboos@edgewall.org>
- #         Alec Thomas <alec@swapoff.org>
-+from collections import defaultdict
- 
- from trac.core import *
- from trac.util.translation import _
-@@ -217,7 +218,44 @@
-         """
-         return Resource(realm, id, version, self)
- 
-+class IResourceChangeListener(Interface):
-+    """Extension point interface for components that require notification
-+    when resources are created, modified, or deleted.
- 
-+    'resource' parameters is instance of the a resource e.g. ticket, milestone
-+    etc.
-+    'context' is an action context, may contain author, comment etc. Context
-+    content depends on a resource type.
-+    """
-+
-+    def get_subscribed_resources():
-+        """
-+        Implementation should return iterator of resource types for which
-+        the listener has to be notified.
-+
-+        None or empty list means all types of resources.
-+        """
-+
-+    def resource_created(resource, context):
-+        """
-+        Called when a resource is created.
-+        """
-+
-+    def resource_changed(resource, old_values, context):
-+        """Called when a resource is modified.
-+
-+        `old_values` is a dictionary containing the previous values of the
-+        resource properties that changed. Properties are specific for resource
-+        type.
-+        """
-+
-+    def resource_deleted(resource, context):
-+        """Called when a resource is deleted."""
-+
-+    def resource_version_deleted(resource, context):
-+        """Called when a version of a resource has been deleted."""
-+
-+
- class ResourceSystem(Component):
-     """Resource identification and description manager.
- 
-@@ -226,10 +264,36 @@
-     """
- 
-     resource_managers = ExtensionPoint(IResourceManager)
-+    changed_listeners = ExtensionPoint(IResourceChangeListener)
- 
-     def __init__(self):
-         self._resource_managers_map = None
-+        self._changed_listeners_map = self._map_changed_listeners()
- 
-+    def _map_changed_listeners(self):
-+        changed_listeners_map = defaultdict(list)
-+        for change_listener in self.changed_listeners:
-+            subscribed_types = change_listener.get_subscribed_resources()
-+            if subscribed_types:
-+                for subscribed_type in subscribed_types:
-+                    changed_listeners_map[subscribed_type].append(
-+                        change_listener)
-+            else:
-+                #empty or None means - subscribe for all resources types
-+                changed_listeners_map[None].append(change_listener)
-+        return changed_listeners_map
-+
-+    def _get_listeners_for_resource(self, resource):
-+        listeners = list(self._changed_listeners_map[None])
-+        for type, subscribers in self._changed_listeners_map.iteritems():
-+            if type is not None and isinstance(resource, type):
-+                listeners.extend(subscribers)
-+        return listeners
-+
-+    def _notify(self, method_name, resource, *args):
-+        for listener in self._get_listeners_for_resource(resource):
-+            getattr(listener, method_name)(resource, *args)
-+
-     # Public methods
- 
-     def get_resource_manager(self, realm):
-@@ -255,7 +319,18 @@
-                 realms.append(realm)
-         return realms
- 
-+    def resource_created(self, resource, context=None):
-+        self._notify('resource_created', resource, context)
- 
-+    def resource_changed(self, resource, old_values, context=None):
-+        self._notify('resource_changed', resource, old_values, context)
-+
-+    def resource_deleted(self, resource, context=None):
-+        self._notify('resource_deleted', resource, context)
-+
-+    def resource_version_deleted(self, resource, context=None):
-+        self._notify('resource_version_deleted', resource, context)
-+
- # -- Utilities for manipulating resources in a generic way
- 
- def get_resource_url(env, resource, href, **kwargs):
-Index: trac/wiki/tests/model.py
-===================================================================
---- trac/wiki/tests/model.py	(revision 11766)
-+++ trac/wiki/tests/model.py	(working copy)
-@@ -12,6 +12,7 @@
- from trac.attachment import Attachment
- from trac.core import *
- from trac.test import EnvironmentStub
-+from trac.tests.resource import TestResourceChangeListener
- from trac.util.datefmt import utc, to_utimestamp
- from trac.wiki import WikiPage, IWikiChangeListener
- 
-@@ -267,9 +268,77 @@
-             page = WikiPage(self.env, 'TestPage')
-             self.assertRaises(TracError, page.rename, name)
- 
-+class WikiResourceChangeListenerTestCase(unittest.TestCase):
-+    INITIAL_NAME = "Wiki page 1"
-+    INITIAL_TEXT = "some text"
-+    INITIAL_AUTHOR = "anAuthor"
-+    INITIAL_COMMENT = "some comment"
-+    INITIAL_REMOTE_ADDRESS = "::1"
- 
-+    def setUp(self):
-+        self.env = EnvironmentStub(default_data=True)
-+        self.listener = TestResourceChangeListener(self.env)
-+        self.listener.resource_type = WikiPage
-+        self.listener.callback = self.listener_callback
-+
-+    def tearDown(self):
-+        self.env.reset_db()
-+
-+    def test_change_listener_created(self):
-+        self._create_wiki_page(self.INITIAL_NAME)
-+        self.assertEqual('created', self.listener.action)
-+        self.assertTrue(isinstance(self.listener.resource, WikiPage))
-+        self.assertEqual(self.INITIAL_NAME, self.wiki_name)
-+        self.assertEqual(self.INITIAL_TEXT, self.wiki_text)
-+
-+    def test_change_listener_text_changed(self):
-+        wiki_page = self._create_wiki_page(self.INITIAL_NAME)
-+        CHANGED_TEXT = "some other text"
-+        wiki_page.text = CHANGED_TEXT
-+        wiki_page.save("author1", "renamed_comment", "::2")
-+        self.assertEqual('changed', self.listener.action)
-+        self.assertTrue(isinstance(self.listener.resource, WikiPage))
-+        self.assertEqual(self.INITIAL_NAME, self.wiki_name)
-+        self.assertEqual(CHANGED_TEXT, self.wiki_text)
-+        self.assertEqual({"text":self.INITIAL_TEXT}, self.listener.old_values)
-+
-+    def test_change_listener_renamed(self):
-+        wiki_page = self._create_wiki_page(self.INITIAL_NAME)
-+        CHANGED_NAME = "NewWikiName"
-+        wiki_page.rename(CHANGED_NAME)
-+        self.assertEqual('changed', self.listener.action)
-+        self.assertTrue(isinstance(self.listener.resource, WikiPage))
-+        self.assertEqual(CHANGED_NAME, self.wiki_name)
-+        self.assertEqual(self.INITIAL_TEXT, self.wiki_text)
-+        self.assertEqual({"name":self.INITIAL_NAME}, self.listener.old_values)
-+
-+    def test_change_listener_deleted(self):
-+        wiki_page = self._create_wiki_page(self.INITIAL_NAME)
-+        wiki_page.delete()
-+        self.assertEqual('deleted', self.listener.action)
-+        self.assertTrue(isinstance(self.listener.resource, WikiPage))
-+        self.assertEqual(self.INITIAL_NAME, self.wiki_name)
-+
-+    def _create_wiki_page(self, name=None):
-+        name = name or self.INITIAL_NAME
-+        wiki_page = WikiPage(self.env, name)
-+        wiki_page.text = self.INITIAL_TEXT
-+        wiki_page.save(
-+            self.INITIAL_AUTHOR,
-+            self.INITIAL_COMMENT,
-+            self.INITIAL_REMOTE_ADDRESS)
-+        return wiki_page
-+
-+    def listener_callback(self, action, resource, context, old_values = None):
-+        self.wiki_name = resource.name
-+        self.wiki_text = resource.text
-+
- def suite():
--    return unittest.makeSuite(WikiPageTestCase, 'test')
-+    suite = unittest.TestSuite()
-+    suite.addTest(unittest.makeSuite(WikiPageTestCase, 'test'))
-+    suite.addTest(unittest.makeSuite(
-+        WikiResourceChangeListenerTestCase, 'test'))
-+    return suite
- 
- if __name__ == '__main__':
-     unittest.main(defaultTest='suite')
-Index: trac/wiki/model.py
-===================================================================
---- trac/wiki/model.py	(revision 11766)
-+++ trac/wiki/model.py	(working copy)
-@@ -21,7 +21,7 @@
- from datetime import datetime
- 
- from trac.core import *
--from trac.resource import Resource
-+from trac.resource import Resource, ResourceSystem
- from trac.util.datefmt import from_utimestamp, to_utimestamp, utc
- from trac.util.translation import _
- from trac.wiki.api import WikiSystem, validate_page_name
-@@ -112,10 +112,12 @@
-         if not self.exists:
-             for listener in WikiSystem(self.env).change_listeners:
-                 listener.wiki_page_deleted(self)
-+            ResourceSystem(self.env).resource_deleted(self)
-         else:
-             for listener in WikiSystem(self.env).change_listeners:
-                 if hasattr(listener, 'wiki_page_version_deleted'):
-                     listener.wiki_page_version_deleted(self)
-+            ResourceSystem(self.env).resource_version_deleted(self)
- 
-     def save(self, author, comment, remote_addr, t=None, db=None):
-         """Save a new version of a page.
-@@ -159,6 +161,24 @@
-             else:
-                 listener.wiki_page_changed(self, self.version, t, comment,
-                                            author, remote_addr)
-+        context=dict(
-+            version=self.version,
-+            time=t,
-+            comment=comment,
-+            author=author,
-+            remote_addr=remote_addr)
-+        if self.version == 1:
-+            ResourceSystem(self.env).resource_created(self, context)
-+        else:
-+            old_values = dict()
-+            if self.readonly != self.old_readonly:
-+                old_values["readonly"] = self.old_readonly
-+            if self.text != self.old_text:
-+                old_values["text"] = self.old_text
-+            ResourceSystem(self.env).resource_changed(
-+                self,
-+                old_values,
-+                context)
- 
-         self.old_readonly = self.readonly
-         self.old_text = self.text
-@@ -196,6 +216,11 @@
-             if hasattr(listener, 'wiki_page_renamed'):
-                 listener.wiki_page_renamed(self, old_name)
- 
-+        ResourceSystem(self.env).resource_changed(
-+            self,
-+            dict(name=old_name)
-+        )
-+
-     def get_history(self, db=None):
-         """Retrieve the edit history of a wiki page.
- 
-Index: trac/attachment.py
-===================================================================
---- trac/attachment.py	(revision 11766)
-+++ trac/attachment.py	(working copy)
-@@ -237,6 +237,7 @@
- 
-         for listener in AttachmentModule(self.env).change_listeners:
-             listener.attachment_deleted(self)
-+        ResourceSystem(self.env).resource_deleted(self)
- 
-     def reparent(self, new_realm, new_id):
-         assert self.filename, "Cannot reparent non-existent attachment"
-@@ -287,6 +288,12 @@
-         for listener in AttachmentModule(self.env).change_listeners:
-             if hasattr(listener, 'attachment_reparented'):
-                 listener.attachment_reparented(self, old_realm, old_id)
-+        old_values = dict()
-+        if self.parent_realm != old_realm:
-+            old_values["parent_realm"] = old_realm
-+        if self.parent_id != old_id:
-+            old_values["parent_id"] = old_id
-+        ResourceSystem(self.env).resource_changed(self, old_values=old_values)
- 
-     def insert(self, filename, fileobj, size, t=None, db=None):
-         """Create a new Attachment record and save the file content.
-@@ -332,6 +339,7 @@
- 
-         for listener in AttachmentModule(self.env).change_listeners:
-             listener.attachment_added(self)
-+        ResourceSystem(self.env).resource_created(self)
- 
- 
-     @classmethod
-Index: trac/ticket/model.py
-===================================================================
---- trac/ticket/model.py	(revision 11766)
-+++ trac/ticket/model.py	(working copy)
-@@ -26,7 +26,7 @@
- from trac import core
- from trac.cache import cached
- from trac.core import TracError
--from trac.resource import Resource, ResourceNotFound
-+from trac.resource import Resource, ResourceNotFound, ResourceSystem
- from trac.ticket.api import TicketSystem
- from trac.util import embedded_numbers, partition
- from trac.util.text import empty
-@@ -286,6 +286,7 @@
- 
-         for listener in TicketSystem(self.env).change_listeners:
-             listener.ticket_created(self)
-+        ResourceSystem(self.env).resource_created(self)
- 
-         return self.id
- 
-@@ -399,6 +400,9 @@
- 
-         for listener in TicketSystem(self.env).change_listeners:
-             listener.ticket_changed(self, comment, author, old_values)
-+        context = dict(comment=comment, author=author)
-+        ResourceSystem(self.env).resource_changed(self, old_values, context)
-+
-         return int(cnum.rsplit('.', 1)[-1])
- 
-     def _to_db_types(self, values):
-@@ -476,6 +480,7 @@
- 
-         for listener in TicketSystem(self.env).change_listeners:
-             listener.ticket_deleted(self)
-+        ResourceSystem(self.env).resource_deleted(self)
- 
-     def get_change(self, cnum=None, cdate=None, db=None):
-         """Return a ticket change by its number or date.
-@@ -761,6 +766,8 @@
-                 except ValueError:
-                     pass # Ignore cast error for this non-essential operation
-             TicketSystem(self.env).reset_ticket_fields()
-+
-+        ResourceSystem(self.env).resource_deleted(self)
-         self.value = self._old_value = None
-         self.name = self._old_name = None
- 
-@@ -788,6 +795,7 @@
- 
-         self._old_name = self.name
-         self._old_value = self.value
-+        ResourceSystem(self.env).resource_created(self)
- 
-     def update(self, db=None):
-         """Update the enum value.
-@@ -811,8 +819,14 @@
-                    (self.name, self._old_name))
-             TicketSystem(self.env).reset_ticket_fields()
- 
-+        old_values = dict()
-+        if self.name != self._old_name:
-+            old_values["name"] = self._old_name
-+        if self.value != self._old_value:
-+            old_values["value"] = self._old_value
-         self._old_name = self.name
-         self._old_value = self.value
-+        ResourceSystem(self.env).resource_changed(self, old_values)
- 
-     @classmethod
-     def select(cls, env, db=None):
-@@ -893,9 +907,11 @@
-         with self.env.db_transaction as db:
-             self.env.log.info("Deleting component %s", self.name)
-             db("DELETE FROM component WHERE name=%s", (self.name,))
--            self.name = self._old_name = None
-             TicketSystem(self.env).reset_ticket_fields()
- 
-+        ResourceSystem(self.env).resource_deleted(self)
-+        self.name = self._old_name = None
-+
-     def insert(self, db=None):
-         """Insert a new component.
- 
-@@ -915,6 +931,8 @@
-             self._old_name = self.name
-             TicketSystem(self.env).reset_ticket_fields()
- 
-+        ResourceSystem(self.env).resource_created(self)
-+
-     def update(self, db=None):
-         """Update the component.
- 
-@@ -926,6 +944,7 @@
-         if not self.name:
-             raise TracError(_("Invalid component name."))
- 
-+        old_name = self._old_name
-         with self.env.db_transaction as db:
-             self.env.log.info("Updating component '%s'", self.name)
-             db("""UPDATE component SET name=%s,owner=%s, description=%s
-@@ -939,6 +958,12 @@
-                 self._old_name = self.name
-             TicketSystem(self.env).reset_ticket_fields()
- 
-+        #todo:add support of old_values for owner and description fields
-+        old_values = dict()
-+        if self.name != old_name:
-+            old_values["name"] = old_name
-+        ResourceSystem(self.env).resource_changed(self, old_values)
-+
-     @classmethod
-     def select(cls, env, db=None):
-         """
-@@ -1075,6 +1100,7 @@
- 
-         for listener in TicketSystem(self.env).milestone_change_listeners:
-             listener.milestone_deleted(self)
-+        ResourceSystem(self.env).resource_deleted(self)
- 
-     def insert(self, db=None):
-         """Insert a new milestone.
-@@ -1097,6 +1123,7 @@
- 
-         for listener in TicketSystem(self.env).milestone_change_listeners:
-             listener.milestone_created(self)
-+        ResourceSystem(self.env).resource_created(self)
- 
-     def update(self, db=None):
-         """Update the milestone.
-@@ -1136,6 +1163,7 @@
-                           if getattr(self, k) != v)
-         for listener in TicketSystem(self.env).milestone_change_listeners:
-             listener.milestone_changed(self, old_values)
-+        ResourceSystem(self.env).resource_changed(self, old_values)
- 
-     @classmethod
-     def select(cls, env, include_completed=True, db=None):
-@@ -1199,9 +1227,11 @@
-         with self.env.db_transaction as db:
-             self.env.log.info("Deleting version %s", self.name)
-             db("DELETE FROM version WHERE name=%s", (self.name,))
--            self.name = self._old_name = None
-             TicketSystem(self.env).reset_ticket_fields()
- 
-+        ResourceSystem(self.env).resource_deleted(self)
-+        self.name = self._old_name = None
-+
-     def insert(self, db=None):
-         """Insert a new version.
- 
-@@ -1220,6 +1250,8 @@
-             self._old_name = self.name
-             TicketSystem(self.env).reset_ticket_fields()
- 
-+        ResourceSystem(self.env).resource_created(self)
-+
-     def update(self, db=None):
-         """Update the version.
- 
-@@ -1231,6 +1263,7 @@
-         if not self.name:
-             raise TracError(_("Invalid version name."))
- 
-+        old_name=self._old_name
-         with self.env.db_transaction as db:
-             self.env.log.info("Updating version '%s'", self.name)
-             db("""UPDATE version
-@@ -1244,6 +1277,12 @@
-                 self._old_name = self.name
-             TicketSystem(self.env).reset_ticket_fields()
- 
-+        #todo: add support of old_values for time and description fields
-+        old_values = dict()
-+        if self.name != old_name:
-+            old_values["name"] = old_name
-+        ResourceSystem(self.env).resource_changed(self, old_values)
-+
-     @classmethod
-     def select(cls, env, db=None):
-         """
-Index: trac/ticket/tests/model.py
-===================================================================
---- trac/ticket/tests/model.py	(revision 11766)
-+++ trac/ticket/tests/model.py	(working copy)
-@@ -18,6 +18,7 @@
-     IMilestoneChangeListener, ITicketChangeListener, TicketSystem
- )
- from trac.test import EnvironmentStub
-+from trac.tests.resource import TestResourceChangeListener
- from trac.util.datefmt import from_utimestamp, to_utimestamp, utc
- 
- 
-@@ -1097,7 +1098,108 @@
-         self.assertEqual([('Test', 0, 'Some text')], self.env.db_query(
-             "SELECT name, time, description FROM version WHERE name='Test'"))
- 
-+class BaseResourceChangeListenerTestCase(unittest.TestCase):
-+    DUMMY_RESOURCE_NAME = "Resource 1"
-+    resource_type = None
-+    name_field = "name"
- 
-+    def setUp(self):
-+        self.env = EnvironmentStub(default_data=True)
-+        self.listener = TestResourceChangeListener(self.env)
-+        self.listener.resource_type = self.resource_type
-+        self.listener.callback = self.listener_callback
-+
-+    def tearDown(self):
-+        self.env.reset_db()
-+
-+    def test_change_listener_created(self):
-+        self._create_resource(self.DUMMY_RESOURCE_NAME)
-+        self.assertEqual('created', self.listener.action)
-+        self.assertTrue(isinstance(self.listener.resource, self.resource_type))
-+        self.assertEqual(
-+            self.DUMMY_RESOURCE_NAME,
-+            self.resource_name)
-+
-+    def test_change_listener_changed(self):
-+        resource = self._create_resource(self.DUMMY_RESOURCE_NAME)
-+        self._rename_resource(resource, "UpdatedName")
-+        self.assertEqual('changed', self.listener.action)
-+        self.assertTrue(isinstance(self.listener.resource, self.resource_type))
-+        self.assertEqual("UpdatedName", self.resource_name)
-+        self.assertEqual(
-+            self.DUMMY_RESOURCE_NAME,
-+            self.listener.old_values[self.name_field])
-+
-+    def test_change_listener_deleted(self):
-+        resource = self._create_resource(self.DUMMY_RESOURCE_NAME)
-+        resource.delete()
-+        self.assertEqual('deleted', self.listener.action)
-+        self.assertTrue(isinstance(self.listener.resource, self.resource_type))
-+        self.assertEqual(self.DUMMY_RESOURCE_NAME, self.resource_name)
-+
-+    def _create_resource(self, name):
-+        resource = self.resource_type(self.env)
-+        resource.name = name
-+        resource.insert()
-+        return resource
-+
-+    def _rename_resource(self, resource, new_name):
-+        resource.name = new_name
-+        resource.update()
-+        return resource
-+
-+    def _get_resource_name(self, resource):
-+        return resource.name
-+
-+    def listener_callback(self, action, resource, context, old_values = None):
-+        self.resource_name = self._get_resource_name(resource)
-+
-+class ComponentResourceChangeListenerTestCase(
-+    BaseResourceChangeListenerTestCase):
-+    resource_type = Component
-+
-+class VersionResourceChangeListenerTestCase(
-+    BaseResourceChangeListenerTestCase):
-+    resource_type = Version
-+
-+class PriorityResourceChangeListenerTestCase(
-+    BaseResourceChangeListenerTestCase):
-+    resource_type = Priority
-+
-+class MilestoneResourceChangeListenerTestCase(
-+    BaseResourceChangeListenerTestCase):
-+    resource_type = Milestone
-+
-+class TicketResourceChangeListenerTestCase(
-+    BaseResourceChangeListenerTestCase):
-+    resource_type = Ticket
-+    name_field = "summary"
-+    dummy_author = "anAuthor"
-+    dummy_comment = "some comment"
-+
-+    def test_change_listener_changed(self):
-+        super(
-+            TicketResourceChangeListenerTestCase,
-+            self).test_change_listener_changed()
-+
-+        self.assertEqual(self.dummy_author, self.listener.context["author"])
-+        self.assertEqual(self.dummy_comment, self.listener.context["comment"])
-+
-+
-+    def _create_resource(self, name):
-+        ticket = Ticket(self.env)
-+        ticket["summary"] = name
-+        ticket.insert()
-+        return ticket
-+
-+    def _rename_resource(self, resource, new_name):
-+        resource["summary"] = new_name
-+        resource.save_changes(self.dummy_author, self.dummy_comment)
-+        return resource
-+
-+    def _get_resource_name(self, resource):
-+        return resource["summary"]
-+
- def suite():
-     suite = unittest.TestSuite()
-     suite.addTest(unittest.makeSuite(TicketTestCase, 'test'))
-@@ -1107,6 +1209,16 @@
-     suite.addTest(unittest.makeSuite(MilestoneTestCase, 'test'))
-     suite.addTest(unittest.makeSuite(ComponentTestCase, 'test'))
-     suite.addTest(unittest.makeSuite(VersionTestCase, 'test'))
-+    suite.addTest(unittest.makeSuite(
-+        ComponentResourceChangeListenerTestCase, 'test'))
-+    suite.addTest(unittest.makeSuite(
-+        VersionResourceChangeListenerTestCase, 'test'))
-+    suite.addTest(unittest.makeSuite(
-+        PriorityResourceChangeListenerTestCase, 'test'))
-+    suite.addTest(unittest.makeSuite(
-+        MilestoneResourceChangeListenerTestCase, 'test'))
-+    suite.addTest(unittest.makeSuite(
-+        TicketResourceChangeListenerTestCase, 'test'))
-     return suite
- 
- if __name__ == '__main__':
-Index: trac/tests/attachment.py
-===================================================================
---- trac/tests/attachment.py	(revision 11766)
-+++ trac/tests/attachment.py	(working copy)
-@@ -11,6 +11,7 @@
- from trac.perm import IPermissionPolicy, PermissionCache
- from trac.resource import Resource, resource_exists
- from trac.test import EnvironmentStub
-+from trac.tests.resource import TestResourceChangeListener
- 
- 
- hashes = {
-@@ -222,9 +223,63 @@
-         self.assertTrue(resource_exists(self.env, att.resource))
- 
- 
-+class AttachmentResourceChangeListenerTestCase(unittest.TestCase):
-+    DUMMY_PARENT_REALM = "wiki"
-+    DUMMY_PARENT_ID = "WikiStart"
-+
-+    def setUp(self):
-+        self.env = EnvironmentStub(default_data=True)
-+        self.listener = TestResourceChangeListener(self.env)
-+        self.listener.resource_type = Attachment
-+        self.listener.callback = self.listener_callback
-+
-+    def tearDown(self):
-+        self.env.reset_db()
-+
-+    def test_change_listener_created(self):
-+        attachment = self._create_attachment()
-+        self.assertEqual('created', self.listener.action)
-+        self.assertTrue(isinstance(self.listener.resource, Attachment))
-+        self.assertEqual(attachment.filename, self.filename)
-+        self.assertEqual(attachment.parent_realm, self.parent_realm)
-+        self.assertEqual(attachment.parent_id, self.parent_id)
-+
-+    def test_change_listener_reparent(self):
-+        attachment = self._create_attachment()
-+        attachment.reparent(self.DUMMY_PARENT_REALM, "SomePage")
-+
-+        self.assertEqual('changed', self.listener.action)
-+        self.assertTrue(isinstance(self.listener.resource, Attachment))
-+        self.assertEqual(attachment.filename, self.filename)
-+        self.assertEqual(attachment.parent_realm, self.parent_realm)
-+        self.assertEqual("SomePage", self.parent_id)
-+        self.assertNotIn("parent_realm", self.listener.old_values)
-+        self.assertEqual(
-+            self.DUMMY_PARENT_ID, self.listener.old_values["parent_id"])
-+
-+    def test_change_listener_deleted(self):
-+        attachment = self._create_attachment()
-+        attachment.delete()
-+        self.assertEqual('deleted', self.listener.action)
-+        self.assertTrue(isinstance(self.listener.resource, Attachment))
-+        self.assertEqual(attachment.filename, self.filename)
-+
-+    def _create_attachment(self):
-+        attachment = Attachment(
-+            self.env, self.DUMMY_PARENT_REALM, self.DUMMY_PARENT_ID)
-+        attachment.insert('file.txt', StringIO(''), 1)
-+        return attachment
-+
-+    def listener_callback(self, action, resource, context, old_values = None):
-+        self.parent_realm = resource.parent_realm
-+        self.parent_id = resource.parent_id
-+        self.filename = resource.filename
-+
- def suite():
-     suite = unittest.TestSuite()
-     suite.addTest(unittest.makeSuite(AttachmentTestCase, 'test'))
-+    suite.addTest(unittest.makeSuite(
-+        AttachmentResourceChangeListenerTestCase, 'test'))
-     return suite
- 
- if __name__ == '__main__':
-Index: trac/tests/resource.py
-===================================================================
---- trac/tests/resource.py	(revision 11766)
-+++ trac/tests/resource.py	(working copy)
-@@ -15,6 +15,8 @@
- import unittest
- 
- from trac import resource
-+from trac.resource import IResourceChangeListener
-+from trac.core import implements, Component
- 
- 
- class ResourceTestCase(unittest.TestCase):
-@@ -42,6 +44,44 @@
-         r2.parent = r2.parent(version=42)
-         self.assertNotEqual(r1, r2)
- 
-+class TestResourceChangeListener(Component):
-+    implements(IResourceChangeListener)
-+
-+    def __init__(self):
-+        self.resource_type = None
-+
-+    def callback(self, action, resource, context, old_values = None):
-+        pass
-+
-+    def get_subscribed_resources(self):
-+        return (self.resource_type, )
-+
-+    def resource_created(self, resource, context):
-+        self.action = "created"
-+        self.resource = resource
-+        self.context = context
-+        self.callback(self.action, resource, context)
-+
-+    def resource_changed(self, resource, old_values, context):
-+        self.action = "changed"
-+        self.resource = resource
-+        self.old_values = old_values
-+        self.context = context
-+        self.callback(
-+            self.action, resource, context, old_values=self.old_values)
-+
-+    def resource_deleted(self, resource, context):
-+        self.action = "deleted"
-+        self.resource = resource
-+        self.context = context
-+        self.callback(self.action, resource, context)
-+
-+    def resource_version_deleted(self, resource, context):
-+        self.action = "version_deleted"
-+        self.resource = resource
-+        self.context = context
-+        self.callback(self.action, resource, context)
-+
- def suite():
-     suite = unittest.TestSuite()
-     suite.addTest(doctest.DocTestSuite(resource))