Commits

Olemis Lang  committed 7aebf82

Trac #11148 : Andrej patch in git format , ready for MQ development

  • Participants
  • Parent commits 6994c3e
  • Branches trac_t11148

Comments (0)

Files changed (2)

+t11148/t11148_r11782_IEntityChangeListener_extends_with_prefix.diff
 #t11148/t11148_r11767_IResourceChangeListener.diff
-t11148/t11148_r11782_IEntityChangeListener_extends_with_prefix.diff

File t11148/t11148_r11782_IEntityChangeListener_extends_with_prefix.diff

-Index: trac/ticket/tests/model.py
-===================================================================
---- trac/ticket/tests/model.py	(revision 11782)
-+++ trac/ticket/tests/model.py	(working copy)
+# HG changeset patch
+# Parent 8d0c3223a81860370202a5184fa812081129fe43
+An alternative proposal is based on ticket comments and google groups discussion. The proposal introduces IEntityChangeListener interface and extends_with_prefix instruction.
+
+diff -r 8d0c3223a818 trac/core.py
+--- a/trac/core.py	Thu Apr 18 14:30:21 2013 +0000
++++ b/trac/core.py	Thu Apr 18 19:52:28 2013 -0500
+@@ -165,8 +165,22 @@
+ 
+         locals_.setdefault('_implements', []).extend(interfaces)
+ 
++    @staticmethod
++    def extends_with_prefix(prefix):
++        import sys
++ 
++        frame = sys._getframe(1)
++        locals_ = frame.f_locals
++
++        # Some sanity checks
++        assert locals_ is not frame.f_globals and '__module__' in locals_, \
++            'extends_with_prefix() can only be used in a class definition'
++
++        locals_['_extends_with_prefix'] = prefix
++
+ 
+ implements = Component.implements
++extends_with_prefix = Component.extends_with_prefix
+ 
+ 
+ class ComponentManager(object):
+@@ -236,3 +250,92 @@
+         with the given class will not be available.
+         """
+         return True
++
++class NotificationChangeInfo(object):
++    def __init__(self, comment, author):
++        self.comment = comment
++        self.author = author
++
++class IEntityChangeListener(Interface):
++    """Extension point interface for components that require notification
++    when model entities are created, modified, or deleted.
++
++    * entity: parameter is instance of the a entity e.g. ticket, milestone
++    etc.
++    * changeinfo: is instance of NotificationChangeInfo. The changeinfo
++    parameter content depends on an entity type and may be None.
++    """
++
++    def entity_created(entity, changeinfo = None):
++        """
++        Called when an entity is created.
++        """
++
++    def entity_changed(entity, old_values, changeinfo = None):
++        """Called when an entity is modified.
++
++        `old_values` is a dictionary containing the previous values of the
++        entity properties that changed. Properties are specific for entity
++        type.
++        """
++
++    def entity_deleted(entity, changeinfo = None):
++        """Called when an entity is deleted."""
++
++    def entity_reparented(entity, changeinfo = None):
++        """Called when an entity has been re-parented."""
++
++class ListenerNotifier(Component):
++    METHOD_DELIMITER = "_"
++
++    def _get_listeners_for_interface(self, interface):
++        """Return a list of components that declare to implement the
++        extension point interface.
++        """
++        classes = ComponentMeta._registry.get(interface, ())
++        components = [self.env[cls] for cls in classes]
++        return [c for c in components if c]
++
++    def notify(self, method, *args):
++        interface = method.im_class
++        method_name = method.__name__
++        if interface is None:
++            raise TracError(
++                "Notification interface can not be None. " +
++                "Target method name is %s" % method_name)
++
++        if hasattr(interface, "_extends_with_prefix"):
++            prefix = interface._extends_with_prefix
++            if prefix:
++                prefixed_method_name =  self._get_prefixed_method_name(
++                    method_name, prefix)
++
++        for listener in self._get_listeners_for_interface(interface):
++            if hasattr(listener, prefixed_method_name):
++                method_to_call = prefixed_method_name
++            else:
++                method_to_call = method_name
++            getattr(listener, method_to_call)(*args)
++
++        #TBD: we can also call listeners implemented IEntityChangeListener here
++        #if community will agree that generic event listeners support is needed
++
++    def _get_prefixed_method_name(self, method_name, prefix):
++        """
++        Replaces the first method name part with prefix. For example,
++        for method_name "entity_created" and prefix "xxx" return
++        "xxx_created",
++        for input method_name "created" and prefix "xxx" return
++        "xxx_created",
++        """
++        method_parts = method_name.split(self.METHOD_DELIMITER)
++        if len(method_parts) == 1:
++            #method name contains only one part, just add prefix before
++            # the method name
++            prefixed_method = self.METHOD_DELIMITER.join((prefix, method_name))
++        else:
++            #replace the first part of the method name with prefix
++            method_parts[0] = prefix
++            prefixed_method = self.METHOD_DELIMITER.join(method_parts)
++        return prefixed_method
++
+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 18 19:52:28 2013 -0500
+@@ -25,7 +25,8 @@
+ from trac.attachment import Attachment
+ from trac import core
+ from trac.cache import cached
+-from trac.core import TracError
++from trac.core import (TracError, ListenerNotifier, IEntityChangeListener,
++                       extends_with_prefix)
+ from trac.resource import Resource, ResourceNotFound
+ from trac.ticket.api import TicketSystem
+ from trac.util import embedded_numbers, partition
+@@ -718,6 +719,7 @@
+ class AbstractEnum(object):
+     type = None
+     ticket_col = None
++    change_listener_interface = None
+ 
+     def __init__(self, env, name=None, db=None):
+         if not self.ticket_col:
+@@ -761,6 +763,10 @@
+                 except ValueError:
+                     pass # Ignore cast error for this non-essential operation
+             TicketSystem(self.env).reset_ticket_fields()
++
++        ListenerNotifier(self.env).notify(
++            self.change_listener_interface.entity_deleted,
++            self)
+         self.value = self._old_value = None
+         self.name = self._old_name = None
+ 
+@@ -788,6 +794,9 @@
+ 
+         self._old_name = self.name
+         self._old_value = self.value
++        ListenerNotifier(self.env).notify(
++            self.change_listener_interface.entity_created,
++            self)
+ 
+     def update(self, db=None):
+         """Update the enum value.
+@@ -811,8 +820,15 @@
+                    (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
++        ListenerNotifier(self.env).notify(
++            self.change_listener_interface.entity_changed, self, old_values)
+ 
+     @classmethod
+     def select(cls, env, db=None):
+@@ -830,10 +846,13 @@
+                 obj.value = obj._old_value = value
+                 yield obj
+ 
++class ITypeChangeListener(IEntityChangeListener):
++    extends_with_prefix("type")
+ 
+ class Type(AbstractEnum):
+     type = 'ticket_type'
+     ticket_col = 'type'
++    change_listener_interface = ITypeChangeListener
+ 
+ 
+ class Status(object):
+@@ -847,19 +866,33 @@
+             status.name = state
+             yield status
+ 
++class IResolutionChangeListener(IEntityChangeListener):
++    extends_with_prefix("resolution")
+ 
+ class Resolution(AbstractEnum):
+     type = 'resolution'
++    change_listener_interface = IResolutionChangeListener
+ 
+ 
++class IPriorityChangeListener(IEntityChangeListener):
++    extends_with_prefix("priority")
++
+ class Priority(AbstractEnum):
+     type = 'priority'
++    change_listener_interface = IPriorityChangeListener
+ 
+ 
++class ISeverityChangeListener(IEntityChangeListener):
++    extends_with_prefix("severity")
++
+ class Severity(AbstractEnum):
+     type = 'severity'
++    change_listener_interface = ISeverityChangeListener
+ 
+ 
++class IComponentChangeListener(IEntityChangeListener):
++    extends_with_prefix("component")
++
+ class Component(object):
+     def __init__(self, env, name=None, db=None):
+         """
+@@ -893,9 +926,13 @@
+         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()
+ 
++        ListenerNotifier(self.env).notify(
++            IComponentChangeListener.entity_deleted, self)
++
++        self.name = self._old_name = None
++
+     def insert(self, db=None):
+         """Insert a new component.
+ 
+@@ -915,6 +952,9 @@
+             self._old_name = self.name
+             TicketSystem(self.env).reset_ticket_fields()
+ 
++        ListenerNotifier(self.env).notify(
++            IComponentChangeListener.entity_created, self)
++
+     def update(self, db=None):
+         """Update the component.
+ 
+@@ -926,6 +966,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 +980,13 @@
+                 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
++        ListenerNotifier(self.env).notify(
++            IComponentChangeListener.entity_changed, self, old_values)
++
+     @classmethod
+     def select(cls, env, db=None):
+         """
+@@ -1169,6 +1217,8 @@
+         groups.append((_('Closed'), closed_milestones))
+     return groups
+ 
++class IVersionChangeListener(IEntityChangeListener):
++    extends_with_prefix("version")
+ 
+ class Version(object):
+     def __init__(self, env, name=None, db=None):
+@@ -1199,9 +1249,12 @@
+         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()
+ 
++        ListenerNotifier(self.env).notify(
++            IVersionChangeListener.entity_deleted, self)
++        self.name = self._old_name = None
++
+     def insert(self, db=None):
+         """Insert a new version.
+ 
+@@ -1220,6 +1273,9 @@
+             self._old_name = self.name
+             TicketSystem(self.env).reset_ticket_fields()
+ 
++        ListenerNotifier(self.env).notify(
++            IVersionChangeListener.entity_created, self)
++
+     def update(self, db=None):
+         """Update the version.
+ 
+@@ -1231,6 +1287,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 +1301,14 @@
+                 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
++
++        ListenerNotifier(self.env).notify(
++            IVersionChangeListener.entity_changed, self, old_values)
++
+     @classmethod
+     def select(cls, env, db=None):
+         """
+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 18 19:52:28 2013 -0500
 @@ -12,8 +12,8 @@
  from trac.core import TracError, implements
  from trac.resource import ResourceNotFound
  from trac.ticket.api import (
      IMilestoneChangeListener, ITicketChangeListener, TicketSystem
  )
-@@ -1097,7 +1097,170 @@
+@@ -1097,6 +1097,169 @@
          self.assertEqual([('Test', 0, 'Some text')], self.env.db_query(
              "SELECT name, time, description FROM version WHERE name='Test'"))
  
 +class EntityChangeListenerMock(core.Component):
- 
++ 
 +    def callback(self, action, entity, changeinfo, old_values = None):
 +        pass
 +
 +        entity.insert()
 +        return entity
 +
-+
+ 
  def suite():
      suite = unittest.TestSuite()
-     suite.addTest(unittest.makeSuite(TicketTestCase, 'test'))
 @@ -1107,6 +1270,14 @@
      suite.addTest(unittest.makeSuite(MilestoneTestCase, 'test'))
      suite.addTest(unittest.makeSuite(ComponentTestCase, 'test'))
      return suite
  
  if __name__ == '__main__':
-Index: trac/ticket/model.py
-===================================================================
---- trac/ticket/model.py	(revision 11782)
-+++ trac/ticket/model.py	(working copy)
-@@ -25,7 +25,8 @@
- from trac.attachment import Attachment
- from trac import core
- from trac.cache import cached
--from trac.core import TracError
-+from trac.core import (TracError, ListenerNotifier, IEntityChangeListener,
-+                       extends_with_prefix)
- from trac.resource import Resource, ResourceNotFound
- from trac.ticket.api import TicketSystem
- from trac.util import embedded_numbers, partition
-@@ -718,6 +719,7 @@
- class AbstractEnum(object):
-     type = None
-     ticket_col = None
-+    change_listener_interface = None
- 
-     def __init__(self, env, name=None, db=None):
-         if not self.ticket_col:
-@@ -761,6 +763,10 @@
-                 except ValueError:
-                     pass # Ignore cast error for this non-essential operation
-             TicketSystem(self.env).reset_ticket_fields()
-+
-+        ListenerNotifier(self.env).notify(
-+            self.change_listener_interface.entity_deleted,
-+            self)
-         self.value = self._old_value = None
-         self.name = self._old_name = None
- 
-@@ -788,6 +794,9 @@
- 
-         self._old_name = self.name
-         self._old_value = self.value
-+        ListenerNotifier(self.env).notify(
-+            self.change_listener_interface.entity_created,
-+            self)
- 
-     def update(self, db=None):
-         """Update the enum value.
-@@ -811,8 +820,15 @@
-                    (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
-+        ListenerNotifier(self.env).notify(
-+            self.change_listener_interface.entity_changed, self, old_values)
- 
-     @classmethod
-     def select(cls, env, db=None):
-@@ -830,10 +846,13 @@
-                 obj.value = obj._old_value = value
-                 yield obj
- 
-+class ITypeChangeListener(IEntityChangeListener):
-+    extends_with_prefix("type")
- 
- class Type(AbstractEnum):
-     type = 'ticket_type'
-     ticket_col = 'type'
-+    change_listener_interface = ITypeChangeListener
- 
- 
- class Status(object):
-@@ -847,19 +866,33 @@
-             status.name = state
-             yield status
- 
-+class IResolutionChangeListener(IEntityChangeListener):
-+    extends_with_prefix("resolution")
- 
- class Resolution(AbstractEnum):
-     type = 'resolution'
-+    change_listener_interface = IResolutionChangeListener
- 
- 
-+class IPriorityChangeListener(IEntityChangeListener):
-+    extends_with_prefix("priority")
-+
- class Priority(AbstractEnum):
-     type = 'priority'
-+    change_listener_interface = IPriorityChangeListener
- 
- 
-+class ISeverityChangeListener(IEntityChangeListener):
-+    extends_with_prefix("severity")
-+
- class Severity(AbstractEnum):
-     type = 'severity'
-+    change_listener_interface = ISeverityChangeListener
- 
- 
-+class IComponentChangeListener(IEntityChangeListener):
-+    extends_with_prefix("component")
-+
- class Component(object):
-     def __init__(self, env, name=None, db=None):
-         """
-@@ -893,9 +926,13 @@
-         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()
- 
-+        ListenerNotifier(self.env).notify(
-+            IComponentChangeListener.entity_deleted, self)
-+
-+        self.name = self._old_name = None
-+
-     def insert(self, db=None):
-         """Insert a new component.
- 
-@@ -915,6 +952,9 @@
-             self._old_name = self.name
-             TicketSystem(self.env).reset_ticket_fields()
- 
-+        ListenerNotifier(self.env).notify(
-+            IComponentChangeListener.entity_created, self)
-+
-     def update(self, db=None):
-         """Update the component.
- 
-@@ -926,6 +966,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 +980,13 @@
-                 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
-+        ListenerNotifier(self.env).notify(
-+            IComponentChangeListener.entity_changed, self, old_values)
-+
-     @classmethod
-     def select(cls, env, db=None):
-         """
-@@ -1169,6 +1217,8 @@
-         groups.append((_('Closed'), closed_milestones))
-     return groups
- 
-+class IVersionChangeListener(IEntityChangeListener):
-+    extends_with_prefix("version")
- 
- class Version(object):
-     def __init__(self, env, name=None, db=None):
-@@ -1199,9 +1249,12 @@
-         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()
- 
-+        ListenerNotifier(self.env).notify(
-+            IVersionChangeListener.entity_deleted, self)
-+        self.name = self._old_name = None
-+
-     def insert(self, db=None):
-         """Insert a new version.
- 
-@@ -1220,6 +1273,9 @@
-             self._old_name = self.name
-             TicketSystem(self.env).reset_ticket_fields()
- 
-+        ListenerNotifier(self.env).notify(
-+            IVersionChangeListener.entity_created, self)
-+
-     def update(self, db=None):
-         """Update the version.
- 
-@@ -1231,6 +1287,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 +1301,14 @@
-                 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
-+
-+        ListenerNotifier(self.env).notify(
-+            IVersionChangeListener.entity_changed, self, old_values)
-+
-     @classmethod
-     def select(cls, env, db=None):
-         """
-Index: trac/core.py
-===================================================================
---- trac/core.py	(revision 11782)
-+++ trac/core.py	(working copy)
-@@ -165,8 +165,21 @@
- 
-         locals_.setdefault('_implements', []).extend(interfaces)
- 
-+    @staticmethod
-+    def extends_with_prefix(prefix):
-+        import sys
- 
-+        frame = sys._getframe(1)
-+        locals_ = frame.f_locals
-+
-+        # Some sanity checks
-+        assert locals_ is not frame.f_globals and '__module__' in locals_, \
-+            'extends_with_prefix() can only be used in a class definition'
-+
-+        locals_['_extends_with_prefix'] = prefix
-+
- implements = Component.implements
-+extends_with_prefix = Component.extends_with_prefix
- 
- 
- class ComponentManager(object):
-@@ -236,3 +249,91 @@
-         with the given class will not be available.
-         """
-         return True
-+
-+class NotificationChangeInfo(object):
-+    def __init__(self, comment, author):
-+        self.comment = comment
-+        self.author = author
-+
-+class IEntityChangeListener(Interface):
-+    """Extension point interface for components that require notification
-+    when model entities are created, modified, or deleted.
-+
-+    * entity: parameter is instance of the a entity e.g. ticket, milestone
-+    etc.
-+    * changeinfo: is instance of NotificationChangeInfo. The changeinfo
-+    parameter content depends on an entity type and may be None.
-+    """
-+
-+    def entity_created(entity, changeinfo = None):
-+        """
-+        Called when an entity is created.
-+        """
-+
-+    def entity_changed(entity, old_values, changeinfo = None):
-+        """Called when an entity is modified.
-+
-+        `old_values` is a dictionary containing the previous values of the
-+        entity properties that changed. Properties are specific for entity
-+        type.
-+        """
-+
-+    def entity_deleted(entity, changeinfo = None):
-+        """Called when an entity is deleted."""
-+
-+    def entity_reparented(entity, changeinfo = None):
-+        """Called when an entity has been re-parented."""
-+
-+class ListenerNotifier(Component):
-+    METHOD_DELIMITER = "_"
-+
-+    def _get_listeners_for_interface(self, interface):
-+        """Return a list of components that declare to implement the
-+        extension point interface.
-+        """
-+        classes = ComponentMeta._registry.get(interface, ())
-+        components = [self.env[cls] for cls in classes]
-+        return [c for c in components if c]
-+
-+    def notify(self, method, *args):
-+        interface = method.im_class
-+        method_name = method.__name__
-+        if interface is None:
-+            raise TracError(
-+                "Notification interface can not be None. " +
-+                "Target method name is %s" % method_name)
-+
-+        if hasattr(interface, "_extends_with_prefix"):
-+            prefix = interface._extends_with_prefix
-+            if prefix:
-+                prefixed_method_name =  self._get_prefixed_method_name(
-+                    method_name, prefix)
-+
-+        for listener in self._get_listeners_for_interface(interface):
-+            if hasattr(listener, prefixed_method_name):
-+                method_to_call = prefixed_method_name
-+            else:
-+                method_to_call = method_name
-+            getattr(listener, method_to_call)(*args)
-+
-+        #TBD: we can also call listeners implemented IEntityChangeListener here
-+        #if community will agree that generic event listeners support is needed
-+
-+    def _get_prefixed_method_name(self, method_name, prefix):
-+        """
-+        Replaces the first method name part with prefix. For example,
-+        for method_name "entity_created" and prefix "xxx" return
-+        "xxx_created",
-+        for input method_name "created" and prefix "xxx" return
-+        "xxx_created",
-+        """
-+        method_parts = method_name.split(self.METHOD_DELIMITER)
-+        if len(method_parts) == 1:
-+            #method name contains only one part, just add prefix before
-+            # the method name
-+            prefixed_method = self.METHOD_DELIMITER.join((prefix, method_name))
-+        else:
-+            #replace the first part of the method name with prefix
-+            method_parts[0] = prefix
-+            prefixed_method = self.METHOD_DELIMITER.join(method_parts)
-+        return prefixed_method
-\ No newline at end of file