Hi everyone, We are implementing a free-text search plugin as a part of Apache™ Bloodhound. The approach we use is based on AdvancedSearch [1] and uses similar approach as TracAdvancedSearchPlugin [2] and FullTextSearchPlugin [3] plugins. In short: listening for ITicketChangeListener events and updating nonDb free-text-search index.
However, there is a problem common for such plugins - renaming of a ticket related entity such as Version, Component Type, Severity etc. is not covered by the event and the plugin cannot reflect this change in its nonDb free-text-search index. As a part of Apache™ Bloodhound Trac fork, we added a new IResourceChangeListener interface, which is common for all resources (see full code in attached patch). The main idea of the IResourceChangeListener interface is that it is similar to existing I*ChangeListener interfaces (existing interfaces are working as before), it is common for all resources (resource is sent as parameter) and that there is no need to introduce interfaces such as IVersionChangeListener, IComponentChangeListener etc. Listener may specificy resource types for which it has to be notified via method get_subscribed_resources. What do you think about such approach? The IResourceChangeListener implementation was tested in Apache™ Bloodhound for a couple of months and it looks like it is stable enough and covers, at least, our requirements. I think that support of the IResourceChangeListener interface in Trac can be useful for Trac community and would like to suggest this patch for Trac. Please find the attached patch rebased against the Trac trunk. Is the Trac community interested in applying something like IResourceChangeListener into the Trac codebase? Cheers, Andrej [1] http://trac.edgewall.org/wiki/AdvancedSearch [2] http://trac-hacks.org/wiki/TracAdvancedSearchPlugin [3] http://trac-hacks.org/wiki/FullTextSearchPlugin -- You received this message because you are subscribed to the Google Groups "Trac Development" group. To unsubscribe from this group and stop receiving emails from it, send an email to [email protected]. To post to this group, send email to [email protected]. Visit this group at http://groups.google.com/group/trac-dev?hl=en. For more options, visit https://groups.google.com/groups/opt_out.
Index: trac/resource.py =================================================================== --- trac/resource.py (revision 11766) +++ trac/resource.py (working copy) @@ -15,6 +15,7 @@ # # Author: Christian Boos <[email protected]> # Alec Thomas <[email protected]> +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))
