← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~wgrant/launchpad/buglinktarget-linkedevent into lp:launchpad

 

William Grant has proposed merging lp:~wgrant/launchpad/buglinktarget-linkedevent into lp:launchpad with lp:~wgrant/launchpad/buglinktarget-no-links as a prerequisite.

Commit message:
Replace IBugLink events with link events on the IBug and IBugLinkTarget.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~wgrant/launchpad/buglinktarget-linkedevent/+merge/272364

Replace IBugLink events with link events on the IBug and IBugLinkTarget.

I've introduced IObjectLinkedEvent and IObjectUnlinkedEvent, currently experimental, living in lp.bugs, and using a private misspelled lazr.lifecycle class. All existing IBugLink IObjectCreatedEvent and IObjectDeletedEvent subscribers are replaced. The subscribers check the linked object interface themselves, but I might later end up letting that be declared in ZCML instead.

The IBugLink implementations will shortly die as part of the XRef series. IBugLink itself dies here.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~wgrant/launchpad/buglinktarget-linkedevent into lp:launchpad.
=== modified file 'lib/lp/answers/model/question.py'
--- lib/lp/answers/model/question.py	2015-09-25 10:42:33 +0000
+++ lib/lp/answers/model/question.py	2015-09-25 10:42:34 +0000
@@ -661,11 +661,11 @@
         return tktmsg
 
     # IBugLinkTarget implementation
-    def linkBug(self, bug):
+    def linkBug(self, bug, user=None):
         """See `IBugLinkTarget`."""
         # Subscribe the question's owner to the bug.
         bug.subscribe(self.owner, self.owner)
-        return BugLinkTargetMixin.linkBug(self, bug)
+        return BugLinkTargetMixin.linkBug(self, bug, user=user)
 
     def unlinkBug(self, bug):
         """See `IBugLinkTarget`."""
@@ -677,14 +677,15 @@
 
     def createBugLink(self, bug):
         """See BugLinkTargetMixin."""
-        return QuestionBug(question=self, bug=bug)
+        QuestionBug(question=self, bug=bug)
 
     def deleteBugLink(self, bug):
         """See BugLinkTargetMixin."""
         link = Store.of(self).find(QuestionBug, question=self, bug=bug).one()
         if link is not None:
             Store.of(link).remove(link)
-        return link
+            return True
+        return False
 
     def setCommentVisibility(self, user, comment_number, visible):
         """See `IQuestion`."""

=== modified file 'lib/lp/blueprints/configure.zcml'
--- lib/lp/blueprints/configure.zcml	2015-09-25 10:42:33 +0000
+++ lib/lp/blueprints/configure.zcml	2015-09-25 10:42:34 +0000
@@ -197,11 +197,6 @@
                     setWorkItems"/>
   </class>
 
-  <class class="lp.blueprints.model.specificationbug.SpecificationBug">
-    <allow interface="lp.blueprints.interfaces.specificationbug.ISpecificationBug"/>
-    <allow interface="lazr.restful.interfaces.IJSONPublishable"/>
-  </class>
-
   <subscriber
       for="lp.blueprints.interfaces.specification.ISpecification
            lazr.lifecycle.interfaces.IObjectCreatedEvent"

=== removed file 'lib/lp/blueprints/interfaces/specificationbug.py'
--- lib/lp/blueprints/interfaces/specificationbug.py	2013-01-07 02:40:55 +0000
+++ lib/lp/blueprints/interfaces/specificationbug.py	1970-01-01 00:00:00 +0000
@@ -1,23 +0,0 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Interfaces for linking between Spec and Bug."""
-
-__metaclass__ = type
-
-__all__ = [
-    'ISpecificationBug',
-    ]
-
-from zope.schema import Object
-
-from lp import _
-from lp.blueprints.interfaces.specification import ISpecification
-from lp.bugs.interfaces.buglink import IBugLink
-
-
-class ISpecificationBug(IBugLink):
-    """A link between a Bug and a specification."""
-
-    specification = Object(title=_('The specification linked to the bug.'),
-        required=True, readonly=True, schema=ISpecification)

=== modified file 'lib/lp/blueprints/model/specification.py'
--- lib/lp/blueprints/model/specification.py	2015-09-25 10:42:33 +0000
+++ lib/lp/blueprints/model/specification.py	2015-09-25 10:42:34 +0000
@@ -793,7 +793,7 @@
 
     def createBugLink(self, bug):
         """See BugLinkTargetMixin."""
-        return SpecificationBug(specification=self, bug=bug)
+        SpecificationBug(specification=self, bug=bug)
 
     def deleteBugLink(self, bug):
         """See BugLinkTargetMixin."""
@@ -801,7 +801,8 @@
             SpecificationBug, specification=self, bug=bug).one()
         if link is not None:
             Store.of(link).remove(link)
-        return link
+            return True
+        return False
 
     # sprint linking
     def linkSprint(self, sprint, user):

=== modified file 'lib/lp/blueprints/model/specificationbug.py'
--- lib/lp/blueprints/model/specificationbug.py	2015-07-08 16:05:11 +0000
+++ lib/lp/blueprints/model/specificationbug.py	2015-09-25 10:42:34 +0000
@@ -5,15 +5,11 @@
 
 __all__ = ['SpecificationBug']
 
-from lazr.restful.interfaces import IJSONPublishable
 from sqlobject import ForeignKey
-from zope.interface import implementer
 
-from lp.blueprints.interfaces.specificationbug import ISpecificationBug
 from lp.services.database.sqlbase import SQLBase
 
 
-@implementer(ISpecificationBug, IJSONPublishable)
 class SpecificationBug(SQLBase):
     """A link between a spec and a bug."""
 
@@ -22,15 +18,3 @@
         foreignKey='Specification', notNull=True)
     bug = ForeignKey(dbName='bug', foreignKey='Bug',
         notNull=True)
-
-    @property
-    def target(self):
-        """See IBugLink."""
-        return self.specification
-
-    def toDataForJSON(self, media_type):
-        """See IJSONPublishable.
-
-        These objects have no JSON representation.
-        """
-        return None

=== modified file 'lib/lp/bugs/configure.zcml'
--- lib/lp/bugs/configure.zcml	2014-11-28 22:07:05 +0000
+++ lib/lp/bugs/configure.zcml	2015-09-25 10:42:34 +0000
@@ -83,10 +83,10 @@
         for="lp.bugs.interfaces.bugbranch.IBugBranch                 lazr.lifecycle.interfaces.IObjectModifiedEvent"
         handler="lp.bugs.subscribers.buglastupdated.update_bug_date_last_updated"/>
     <subscriber
-        for="lp.bugs.interfaces.bugcve.IBugCve                 lazr.lifecycle.interfaces.IObjectCreatedEvent"
+        for="lp.bugs.interfaces.bug.IBug lp.bugs.interfaces.buglink.IObjectLinkedEvent"
         handler="lp.bugs.subscribers.buglastupdated.update_bug_date_last_updated"/>
     <subscriber
-        for="lp.bugs.interfaces.bugcve.IBugCve                 lazr.lifecycle.interfaces.IObjectCreatedEvent"
+        for="lp.bugs.interfaces.bug.IBug lp.bugs.interfaces.buglink.IObjectLinkedEvent"
         handler="lp.bugs.subscribers.karma.cve_added"/>
     <subscriber
         for="lp.bugs.interfaces.bugmessage.IBugMessage                 lazr.lifecycle.interfaces.IObjectCreatedEvent"
@@ -553,14 +553,6 @@
             interface="lp.bugs.interfaces.bugbranch.IBugBranchSet"/>
     </securedutility>
 
-    <!-- BugCve -->
-
-    <class
-        class="lp.bugs.model.cve.BugCve">
-        <allow
-            interface="lp.bugs.interfaces.bugcve.IBugCve"/>
-    </class>
-
         <!-- CVE -->
 
         <class
@@ -831,10 +823,10 @@
         for="lp.bugs.interfaces.bug.IBug                            zope.lifecycleevent.interfaces.IObjectCreatedEvent"
         handler="lp.bugs.subscribers.bugactivity.record_bug_added"/>
     <subscriber
-        for="lp.bugs.interfaces.bugcve.IBugCve                            lazr.lifecycle.interfaces.IObjectCreatedEvent"
+        for="lp.bugs.interfaces.bug.IBug lp.bugs.interfaces.buglink.IObjectLinkedEvent"
         handler="lp.bugs.subscribers.bugactivity.record_cve_linked_to_bug"/>
     <subscriber
-        for="lp.bugs.interfaces.bugcve.IBugCve                            lazr.lifecycle.interfaces.IObjectDeletedEvent"
+        for="lp.bugs.interfaces.bug.IBug lp.bugs.interfaces.buglink.IObjectUnlinkedEvent"
         handler="lp.bugs.subscribers.bugactivity.record_cve_unlinked_from_bug"/>
     <subscriber
         for="lp.bugs.interfaces.bugsubscription.IBugSubscription                     zope.lifecycleevent.interfaces.IObjectCreatedEvent"

=== modified file 'lib/lp/bugs/doc/bug.txt'
--- lib/lp/bugs/doc/bug.txt	2015-06-26 14:00:41 +0000
+++ lib/lp/bugs/doc/bug.txt	2015-09-25 10:42:34 +0000
@@ -771,19 +771,14 @@
 
     >>> from lp.bugs.interfaces.cve import ICveSet
 
-    >>> firefox_bug.cve_links.count()
+    >>> firefox_bug.cves.count()
     0
+    >>> current_date_last_updated = firefox_bug.date_last_updated
 
     >>> cveref = getUtility(ICveSet)["1999-8979"]
-    >>> bug_cveref = firefox_bug.linkCVE(cveref, sample_person)
-
-    >>> current_date_last_updated = firefox_bug.date_last_updated
-
-    >>> notify(ObjectCreatedEvent(bug_cveref))
-
-    >>> firefox_bug.cve_links.count()
+    >>> firefox_bug.linkCVE(cveref, sample_person)
+    >>> firefox_bug.cves.count()
     1
-
     >>> firefox_bug.date_last_updated > current_date_last_updated
     True
 

=== modified file 'lib/lp/bugs/doc/cve.txt'
--- lib/lp/bugs/doc/cve.txt	2014-03-03 19:42:30 +0000
+++ lib/lp/bugs/doc/cve.txt	2015-09-25 10:42:34 +0000
@@ -51,7 +51,6 @@
     >>> no_priv = getUtility(IPersonSet).getByName('no-priv')
     >>> bug_one = getUtility(IBugSet).get(1)
     >>> bug_one.linkCVE(cve, user=no_priv)
-    <BugCve at ...>
 
     >>> cveset.getBugCveCount()
     3
@@ -79,7 +78,6 @@
     >>> b.cves.count()
     1
     >>> b.linkCVE(cve, no_priv)
-    <BugCve at ...>
     >>> b.cves.count()
     2
 

=== modified file 'lib/lp/bugs/doc/malone-karma.txt'
--- lib/lp/bugs/doc/malone-karma.txt	2012-07-07 21:44:45 +0000
+++ lib/lp/bugs/doc/malone-karma.txt	2015-09-25 10:42:34 +0000
@@ -56,9 +56,9 @@
     >>> from lp.bugs.model.cve import BugCve
     >>> cve = getUtility(ICveSet).new('2003-1234', description="Blah blah",
     ...     status=CveStatus.CANDIDATE)
-    >>> bugcve = BugCve(cve=cve, bug=bug)
-    >>> notify(ObjectCreatedEvent(bugcve))
+    >>> cve.linkBug(bug)
     Karma added: action=bugcverefadded, distribution=debian
+    True
 
 Add watch for external bug to the bug:
 

=== modified file 'lib/lp/bugs/interfaces/bug.py'
--- lib/lp/bugs/interfaces/bug.py	2015-01-30 18:24:07 +0000
+++ lib/lp/bugs/interfaces/bug.py	2015-09-25 10:42:34 +0000
@@ -285,7 +285,6 @@
             value_type=Reference(schema=ICve),
             readonly=True))
     has_cves = Bool(title=u"True if the bug has cve entries.")
-    cve_links = Attribute('Links between this bug and CVE entries.')
     duplicates = exported(doNotSnapshot(
         CollectionField(
             title=_("MultiJoin of bugs which are dupes of this one."),
@@ -813,10 +812,10 @@
         file_alias.restricted.
         """
 
-    @call_with(user=REQUEST_USER, return_cve=False)
+    @call_with(user=REQUEST_USER)
     @operation_parameters(cve=Reference(ICve, title=_('CVE'), required=True))
     @export_write_operation()
-    def linkCVE(cve, user, return_cve=True):
+    def linkCVE(cve, user):
         """Ensure that this CVE is linked to this bug."""
 
     @call_with(user=REQUEST_USER)

=== removed file 'lib/lp/bugs/interfaces/bugcve.py'
--- lib/lp/bugs/interfaces/bugcve.py	2013-01-07 02:40:55 +0000
+++ lib/lp/bugs/interfaces/bugcve.py	1970-01-01 00:00:00 +0000
@@ -1,24 +0,0 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""BugCve linker interfaces."""
-
-__metaclass__ = type
-
-__all__ = ['IBugCve']
-
-from zope.schema import Object
-
-from lp import _
-from lp.bugs.interfaces.buglink import IBugLink
-from lp.bugs.interfaces.cve import ICve
-
-
-class IBugCve(IBugLink):
-    """A link between a bug and a CVE entry."""
-
-    cve = Object(title=_('Cve Sequence'), required=True, readonly=True,
-        description=_("Enter the CVE sequence number (XXXX-XXXX) that "
-        "describes the same issue as this bug is addressing."),
-        schema=ICve)
-

=== modified file 'lib/lp/bugs/interfaces/buglink.py'
--- lib/lp/bugs/interfaces/buglink.py	2015-09-25 10:42:33 +0000
+++ lib/lp/bugs/interfaces/buglink.py	2015-09-25 10:42:34 +0000
@@ -6,9 +6,10 @@
 __metaclass__ = type
 
 __all__ = [
-    'IBugLink',
     'IBugLinkForm',
     'IBugLinkTarget',
+    'IObjectLinkedEvent',
+    'IObjectUnlinkedEvent',
     'IUnlinkBugsForm',
     ]
 
@@ -20,6 +21,7 @@
     CollectionField,
     Reference,
     )
+from zope.component.interfaces import IObjectEvent
 from zope.interface import (
     Attribute,
     implementer,
@@ -27,7 +29,6 @@
     )
 from zope.schema import (
     Choice,
-    Object,
     Set,
     )
 from zope.schema.interfaces import IContextSourceBinder
@@ -39,19 +40,21 @@
 
 from lp import _
 from lp.bugs.interfaces.bug import IBug
-from lp.bugs.interfaces.hasbug import IHasBug
 from lp.services.fields import BugField
 
 
-class IBugLink(IHasBug):
-    """An entity representing a link between a bug and its target."""
-
-    bug = BugField(title=_("The bug that is linked to."),
-                   required=True, readonly=True)
-    bugID = Attribute("Database id of the bug.")
-
-    target = Object(title=_("The object to which the bug is linked."),
-                    required=True, readonly=True, schema=Interface)
+class IObjectLinkedEvent(IObjectEvent):
+    """An object that has been linked to another."""
+
+    other_object = Attribute("The object that is now linked.")
+    user = Attribute("The user who linked the object.")
+
+
+class IObjectUnlinkedEvent(IObjectEvent):
+    """An object that has been unlinked from another."""
+
+    other_object = Attribute("The object that is no longer linked.")
+    user = Attribute("The user who unlinked the object.")
 
 
 class IBugLinkTarget(Interface):
@@ -69,8 +72,8 @@
     def linkBug(bug):
         """Link the object with this bug.
 
-        If a new IBugLink is created by this method, an ObjectCreatedEvent
-        is sent.
+        If a new link is created by this method, an ObjectLinkedEvent is
+        sent for each end.
 
         :return: True if a new link was created, False if it already existed.
         """
@@ -78,8 +81,8 @@
     def unlinkBug(bug):
         """Remove any link between this object and the bug.
 
-        If an IBugLink is removed by this method, an ObjectDeletedEvent
-        is sent.
+        If a link is removed by this method, an ObjectUnlinkedEvent is
+        sent for each end.
 
         :return: True if a link was deleted, False if it didn't exist.
         """

=== modified file 'lib/lp/bugs/mail/tests/test_commands.py'
--- lib/lp/bugs/mail/tests/test_commands.py	2015-01-29 18:43:52 +0000
+++ lib/lp/bugs/mail/tests/test_commands.py	2015-09-25 10:42:34 +0000
@@ -618,7 +618,7 @@
         dummy_event = object()
         exec_bug, event = command.execute(bug, dummy_event)
         self.assertEqual(bug, exec_bug)
-        self.assertEqual([cve], [cve_link.cve for cve_link in bug.cve_links])
+        self.assertContentEqual([cve], bug.cves)
         self.assertEqual(dummy_event, event)
 
     def test_execute_bug_params(self):

=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py	2015-07-08 16:05:11 +0000
+++ lib/lp/bugs/model/bug.py	2015-09-25 10:42:34 +0000
@@ -153,7 +153,6 @@
 from lp.bugs.model.bugactivity import BugActivity
 from lp.bugs.model.bugattachment import BugAttachment
 from lp.bugs.model.bugbranch import BugBranch
-from lp.bugs.model.bugcve import BugCve
 from lp.bugs.model.bugmessage import BugMessage
 from lp.bugs.model.bugnomination import BugNomination
 from lp.bugs.model.bugnotification import BugNotification
@@ -366,7 +365,6 @@
         'BugWatch', joinColumn='bug', orderBy=['bugtracker', 'remotebug'])
     cves = SQLRelatedJoin('Cve', intermediateTable='BugCve',
         orderBy='sequence', joinColumn='bug', otherColumn='cve')
-    cve_links = SQLMultipleJoin('BugCve', joinColumn='bug', orderBy='id')
     duplicates = SQLMultipleJoin('Bug', joinColumn='duplicateof', orderBy='id')
     specifications = SQLRelatedJoin(
         'Specification', joinColumn='bug', otherColumn='specification',
@@ -1377,21 +1375,13 @@
         """See `IBug`."""
         return bool(self.cves)
 
-    def linkCVE(self, cve, user, return_cve=True):
+    def linkCVE(self, cve, user):
         """See `IBug`."""
-        if cve not in self.cves:
-            bugcve = BugCve(bug=self, cve=cve)
-            notify(ObjectCreatedEvent(bugcve, user=user))
-            if return_cve:
-                return bugcve
+        cve.linkBug(self, user=user)
 
     def unlinkCVE(self, cve, user):
         """See `IBug`."""
-        for cve_link in self.cve_links:
-            if cve_link.cve.id == cve.id:
-                notify(ObjectDeletedEvent(cve_link, user=user))
-                BugCve.delete(cve_link.id)
-                break
+        cve.unlinkBug(self, user=user)
 
     def findCvesInText(self, text, user):
         """See `IBug`."""

=== modified file 'lib/lp/bugs/model/bugcve.py'
--- lib/lp/bugs/model/bugcve.py	2015-07-08 16:05:11 +0000
+++ lib/lp/bugs/model/bugcve.py	2015-09-25 10:42:34 +0000
@@ -5,13 +5,10 @@
 __all__ = ['BugCve']
 
 from sqlobject import ForeignKey
-from zope.interface import implementer
 
-from lp.bugs.interfaces.bugcve import IBugCve
 from lp.services.database.sqlbase import SQLBase
 
 
-@implementer(IBugCve)
 class BugCve(SQLBase):
     """A table linking bugs and CVE entries."""
 
@@ -20,8 +17,3 @@
     # db field names
     bug = ForeignKey(dbName='bug', foreignKey='Bug', notNull=True)
     cve = ForeignKey(dbName='cve', foreignKey='Cve', notNull=True)
-
-    @property
-    def target(self):
-        """See IBugLink."""
-        return self.cve

=== modified file 'lib/lp/bugs/model/buglinktarget.py'
--- lib/lp/bugs/model/buglinktarget.py	2015-09-25 10:42:33 +0000
+++ lib/lp/bugs/model/buglinktarget.py	2015-09-25 10:42:34 +0000
@@ -4,16 +4,37 @@
 __metaclass__ = type
 __all__ = [ 'BugLinkTargetMixin' ]
 
-from lazr.lifecycle.event import (
-    ObjectCreatedEvent,
-    ObjectDeletedEvent,
-    )
+import lazr.lifecycle.event
 from zope.event import notify
+from zope.interface import implementer
 from zope.security.interfaces import Unauthorized
 
+from lp.bugs.interfaces.buglink import (
+    IObjectLinkedEvent,
+    IObjectUnlinkedEvent,
+    )
 from lp.services.webapp.authorization import check_permission
 
 
+# XXX wgrant 2015-09-25: lazr.lifecycle.event.LifecyleEventBase is all
+# of mispelled, private, and the sole implementer of user-fetching
+# logic that we require.
+@implementer(IObjectLinkedEvent)
+class ObjectLinkedEvent(lazr.lifecycle.event.LifecyleEventBase):
+
+    def __init__(self, object, other_object, user=None):
+        super(ObjectLinkedEvent, self).__init__(object, user=user)
+        self.other_object = other_object
+
+
+@implementer(IObjectUnlinkedEvent)
+class ObjectUnlinkedEvent(lazr.lifecycle.event.LifecyleEventBase):
+
+    def __init__(self, object, other_object, user=None):
+        super(ObjectUnlinkedEvent, self).__init__(object, user=user)
+        self.other_object = other_object
+
+
 class BugLinkTargetMixin:
     """Mixin class for IBugLinkTarget implementation."""
 
@@ -28,7 +49,7 @@
         raise NotImplementedError("missing deleteBugLink() implementation")
 
     # IBugLinkTarget implementation
-    def linkBug(self, bug):
+    def linkBug(self, bug, user=None):
         """See IBugLinkTarget."""
         # XXX gmb 2007-12-11 bug=175545:
         #     We shouldn't be calling check_permission here. The user's
@@ -41,11 +62,12 @@
                 "cannot link to a private bug you don't have access to")
         if bug in self.bugs:
             return False
-        buglink = self.createBugLink(bug)
-        notify(ObjectCreatedEvent(buglink))
+        self.createBugLink(bug)
+        notify(ObjectLinkedEvent(bug, self, user=user))
+        notify(ObjectLinkedEvent(self, bug, user=user))
         return True
 
-    def unlinkBug(self, bug):
+    def unlinkBug(self, bug, user=None):
         """See IBugLinkTarget."""
         # XXX gmb 2007-12-11 bug=175545:
         #     We shouldn't be calling check_permission here. The user's
@@ -58,8 +80,9 @@
                 "cannot unlink a private bug you don't have access to")
 
         # see if a relevant bug link exists, and if so, delete it
-        buglink = self.deleteBugLink(bug)
-        if buglink is not None:
-            notify(ObjectDeletedEvent(buglink))
+        removed = self.deleteBugLink(bug)
+        if removed:
+            notify(ObjectUnlinkedEvent(bug, self, user=user))
+            notify(ObjectUnlinkedEvent(self, bug, user=user))
             return True
         return False

=== modified file 'lib/lp/bugs/model/cve.py'
--- lib/lp/bugs/model/cve.py	2015-09-25 10:42:33 +0000
+++ lib/lp/bugs/model/cve.py	2015-09-25 10:42:34 +0000
@@ -87,14 +87,15 @@
 
     def createBugLink(self, bug):
         """See BugLinkTargetMixin."""
-        return BugCve(cve=self, bug=bug)
+        BugCve(cve=self, bug=bug)
 
     def deleteBugLink(self, bug):
         """See BugLinkTargetMixin."""
         link = Store.of(self).find(BugCve, cve=self, bug=bug).one()
         if link is not None:
             Store.of(link).remove(link)
-        return link
+            return True
+        return False
 
 
 @implementer(ICveSet)

=== modified file 'lib/lp/bugs/subscribers/bugactivity.py'
--- lib/lp/bugs/subscribers/bugactivity.py	2013-11-29 12:57:28 +0000
+++ lib/lp/bugs/subscribers/bugactivity.py	2015-09-25 10:42:34 +0000
@@ -21,6 +21,7 @@
 from lp.bugs.enums import BugNotificationLevel
 from lp.bugs.interfaces.bug import IBug
 from lp.bugs.interfaces.bugactivity import IBugActivitySet
+from lp.bugs.interfaces.cve import ICve
 from lp.registry.enums import PersonVisibility
 from lp.registry.interfaces.milestone import IMilestone
 from lp.registry.interfaces.person import IPerson
@@ -107,23 +108,23 @@
 
 
 @block_implicit_flushes
-def record_cve_linked_to_bug(bug_cve, event):
+def record_cve_linked_to_bug(bug, event):
     """Record when a CVE is linked to a bug."""
-    bug_cve.bug.addChange(
+    if not ICve.providedBy(event.other_object):
+        return
+    bug.addChange(
         CveLinkedToBug(
-            when=None,
-            person=IPerson(event.user),
-            cve=bug_cve.cve))
+            when=None, person=IPerson(event.user), cve=event.other_object))
 
 
 @block_implicit_flushes
-def record_cve_unlinked_from_bug(bug_cve, event):
+def record_cve_unlinked_from_bug(bug, event):
     """Record when a CVE is unlinked from a bug."""
-    bug_cve.bug.addChange(
+    if not ICve.providedBy(event.other_object):
+        return
+    bug.addChange(
         CveUnlinkedFromBug(
-            when=None,
-            person=IPerson(event.user),
-            cve=bug_cve.cve))
+            when=None, person=IPerson(event.user), cve=event.other_object))
 
 
 @block_implicit_flushes

=== modified file 'lib/lp/bugs/subscribers/karma.py'
--- lib/lp/bugs/subscribers/karma.py	2011-12-30 06:14:56 +0000
+++ lib/lp/bugs/subscribers/karma.py	2015-09-25 10:42:34 +0000
@@ -4,6 +4,7 @@
 """Assign karma for bugs domain activity."""
 
 from lp.bugs.interfaces.bugtask import BugTaskStatus
+from lp.bugs.interfaces.cve import ICve
 from lp.bugs.subscribers.bug import get_bug_delta
 from lp.registry.interfaces.person import IPerson
 from lp.services.database.sqlbase import block_implicit_flushes
@@ -81,10 +82,11 @@
 
 
 @block_implicit_flushes
-def cve_added(cve, event):
+def cve_added(bug, event):
     """Assign karma to the user which added :cve:."""
-    _assignKarmaUsingBugContext(
-        IPerson(event.user), cve.bug, 'bugcverefadded')
+    if not ICve.providedBy(event.other_object):
+        return
+    _assignKarmaUsingBugContext(IPerson(event.user), bug, 'bugcverefadded')
 
 
 @block_implicit_flushes

=== modified file 'lib/lp/bugs/tests/buglinktarget.txt'
--- lib/lp/bugs/tests/buglinktarget.txt	2015-09-25 10:42:33 +0000
+++ lib/lp/bugs/tests/buglinktarget.txt	2015-09-25 10:42:34 +0000
@@ -17,10 +17,7 @@
     >>> login('no-priv@xxxxxxxxxxxxx')
     >>> from zope.interface.verify import verifyObject
     >>> from lp.bugs.interfaces.bug import IBugSet
-    >>> from lp.bugs.interfaces.buglink import (
-    ...     IBugLink,
-    ...     IBugLinkTarget,
-    ...     )
+    >>> from lp.bugs.interfaces.buglink import IBugLinkTarget
 
     >>> verifyObject(IBugLinkTarget, target)
     True
@@ -43,29 +40,36 @@
     >>> target.linkBug(bug1)
     False
 
-When a IBugLink is created, one IObjectCreatedEvent for the created
-should be fired by the method.
+When a bug link is created, an IObjectLinkedEvent for each end should be
+fired.
 
+    >>> from zope.interface import Interface
+    >>> from lp.bugs.interfaces.buglink import (
+    ...     IObjectLinkedEvent, IObjectUnlinkedEvent)
     >>> from lp.testing.event import TestEventListener
-    >>> from lazr.lifecycle.interfaces import (
-    ...     IObjectCreatedEvent, IObjectDeletedEvent)
-    >>> created_events = []
-    >>> created_event_listener = TestEventListener(
-    ...     IBugLink, IObjectCreatedEvent,
-    ...     lambda object, event: created_events.append(event))
+    >>> linked_events = []
+    >>> linked_event_listener = TestEventListener(
+    ...     Interface, IObjectLinkedEvent,
+    ...     lambda object, event: linked_events.append(event))
 
     >>> bug2 = bugset.get(2)
     >>> target.linkBug(bugset.get(2))
     True
-    >>> created_events[-1].object.bug == bug2
-    True
-
-Of course, if no new IBugLink is created, no events should be fired:
-
-    >>> created_events = []
+    >>> linked_events[-2].object == bug2
+    True
+    >>> linked_events[-2].other_object == target
+    True
+    >>> linked_events[-1].object == target
+    True
+    >>> linked_events[-1].other_object == bug2
+    True
+
+Of course, if no new link is created, no events should be fired:
+
+    >>> linked_events = []
     >>> target.linkBug(bug2)
     False
-    >>> created_events
+    >>> linked_events
     []
 
 Anonymous users cannot use linkBug():
@@ -114,17 +118,23 @@
 
     >>> login('no-priv@xxxxxxxxxxxxx')
 
-The method returns the linked object which was removed. It should also
-send a IObjectDeletedEvent for the removed IBugLink:
+The method returns whether the link existed. It should also send an
+IObjectUnlinkedEvent for each of the removed link:
 
-    >>> deleted_events = []
-    >>> deleted_event_listener = TestEventListener(
-    ...     IBugLink, IObjectDeletedEvent,
-    ...     lambda object, event: deleted_events.append(event))
+    >>> unlinked_events = []
+    >>> unlinked_event_listener = TestEventListener(
+    ...     Interface, IObjectUnlinkedEvent,
+    ...     lambda object, event: unlinked_events.append(event))
 
     >>> target.unlinkBug(bug1)
     True
-    >>> deleted_events[-1].object.bug == bug1
+    >>> unlinked_events[-2].object == bug1
+    True
+    >>> unlinked_events[-2].other_object == target
+    True
+    >>> unlinked_events[-1].object == target
+    True
+    >>> unlinked_events[-1].other_object == bug1
     True
 
     >>> [bug.id for bug in target.bugs]
@@ -133,10 +143,10 @@
 When the bug was not linked to the target, that method should return
 False (and not trigger any events):
 
-    >>> deleted_events = []
+    >>> unlinked_events = []
     >>> target.unlinkBug(bug1)
     False
-    >>> deleted_events
+    >>> unlinked_events
     []
 
 A user can only remove a link to a private bug if he is subscribed to
@@ -154,5 +164,5 @@
 == Cleanup ==
 
     # Unregister event listeners.
-    >>> created_event_listener.unregister()
-    >>> deleted_event_listener.unregister()
+    >>> linked_event_listener.unregister()
+    >>> unlinked_event_listener.unregister()

=== modified file 'lib/lp/bugs/tests/test_bug.py'
--- lib/lp/bugs/tests/test_bug.py	2014-11-14 22:10:03 +0000
+++ lib/lp/bugs/tests/test_bug.py	2015-09-25 10:42:34 +0000
@@ -319,7 +319,7 @@
         target = self.factory.makeProduct()
         person = self.factory.makePerson()
         bug = self.createBug(owner=person, target=target, cve=cve)
-        self.assertEqual([cve], [cve_link.cve for cve_link in bug.cve_links])
+        self.assertContentEqual([cve], bug.cves)
 
     def test_createBug_subscribers(self):
         # Bugs normally start with just the reporter subscribed.

=== modified file 'lib/lp/coop/answersbugs/configure.zcml'
--- lib/lp/coop/answersbugs/configure.zcml	2010-10-03 15:30:06 +0000
+++ lib/lp/coop/answersbugs/configure.zcml	2015-09-25 10:42:34 +0000
@@ -9,21 +9,16 @@
   i18n_domain="launchpad">
 <facet facet="answers">
 
-<subscriber
-   for="lp.bugs.interfaces.bugtask.IBugTask
-        lazr.lifecycle.interfaces.IObjectModifiedEvent"
-   handler=".notification.dispatch_linked_question_notifications"
-   />
-
-  <class class=".model.QuestionBug">
-    <allow interface=".interfaces.IQuestionBug"/>
-  </class>
-
-    <subscriber
-        for=".interfaces.IQuestionBug
-             lazr.lifecycle.interfaces.IObjectCreatedEvent"
-        handler=".karma.question_bug_added"
-        />
+  <subscriber
+    for="lp.bugs.interfaces.bugtask.IBugTask
+         lazr.lifecycle.interfaces.IObjectModifiedEvent"
+    handler=".notification.dispatch_linked_question_notifications"
+    />
+
+  <subscriber
+    for="lp.answers.interfaces.question.IQuestion lp.bugs.interfaces.buglink.IObjectLinkedEvent"
+    handler=".karma.question_bug_linked"
+    />
 
   <browser:page
     name="+makebug"

=== removed file 'lib/lp/coop/answersbugs/interfaces.py'
--- lib/lp/coop/answersbugs/interfaces.py	2013-01-07 02:40:55 +0000
+++ lib/lp/coop/answersbugs/interfaces.py	1970-01-01 00:00:00 +0000
@@ -1,23 +0,0 @@
-# Copyright 2009 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Interfaces for linking between an IQuestion and an IBug."""
-
-__metaclass__ = type
-
-__all__ = [
-    'IQuestionBug',
-    ]
-
-from zope.schema import Object
-
-from lp import _
-from lp.answers.interfaces.question import IQuestion
-from lp.bugs.interfaces.buglink import IBugLink
-
-
-class IQuestionBug(IBugLink):
-    """A link between an IBug and an IQuestion."""
-
-    question = Object(title=_('The question to which the bug is linked to.'),
-        required=True, readonly=True, schema=IQuestion)

=== modified file 'lib/lp/coop/answersbugs/karma.py'
--- lib/lp/coop/answersbugs/karma.py	2011-12-30 06:14:56 +0000
+++ lib/lp/coop/answersbugs/karma.py	2015-09-25 10:42:34 +0000
@@ -7,14 +7,14 @@
 __all__ = []
 
 from lp.answers.karma import assignKarmaUsingQuestionContext
+from lp.bugs.interfaces.bug import IBug
 from lp.registry.interfaces.person import IPerson
 from lp.services.database.sqlbase import block_implicit_flushes
 
 
 @block_implicit_flushes
-def question_bug_added(questionbug, event):
+def question_bug_linked(questionbug, event):
     """Assign karma to the user which added <questionbug>."""
-    question = questionbug.question
-    assignKarmaUsingQuestionContext(
-        IPerson(event.user), question, 'questionlinkedtobug')
-
+    if IBug.providedBy(event.other_object):
+        assignKarmaUsingQuestionContext(
+            IPerson(event.user), event.object, 'questionlinkedtobug')

=== modified file 'lib/lp/coop/answersbugs/model.py'
--- lib/lp/coop/answersbugs/model.py	2015-07-08 16:05:11 +0000
+++ lib/lp/coop/answersbugs/model.py	2015-09-25 10:42:34 +0000
@@ -8,13 +8,10 @@
 __all__ = ['QuestionBug']
 
 from sqlobject import ForeignKey
-from zope.interface import implementer
 
-from lp.coop.answersbugs.interfaces import IQuestionBug
 from lp.services.database.sqlbase import SQLBase
 
 
-@implementer(IQuestionBug)
 class QuestionBug(SQLBase):
     """A link between a question and a bug."""
 
@@ -23,9 +20,3 @@
     question = ForeignKey(
         dbName='question', foreignKey='Question', notNull=True)
     bug = ForeignKey(dbName='bug', foreignKey='Bug', notNull=True)
-
-    @property
-    def target(self):
-        """See IBugLink."""
-        return self.question
-


Follow ups