← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~stevenk/launchpad/force-ibug-into-line into lp:launchpad

 

Steve Kowalik has proposed merging lp:~stevenk/launchpad/force-ibug-into-line into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #949672 in Launchpad itself: "IBug needs to be a better interface"
  https://bugs.launchpad.net/launchpad/+bug/949672

For more details, see:
https://code.launchpad.net/~stevenk/launchpad/force-ibug-into-line/+merge/96513

Force IBug into towing the line.

This branch massively cleans up the ZCML related to IBug, and blows it apart into four separate classes: IBugPublic, IBugView, IBugEdit and IBugAdmin.

I have tried my best to keep this branch small, but the size just can't be helped due to how large the ZCML changes are, and how big IBug is.

I have also added a missing method to IBugView which was implemented in the model, listed in the ZCML, but did not appear in the old IBug.
-- 
https://code.launchpad.net/~stevenk/launchpad/force-ibug-into-line/+merge/96513
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~stevenk/launchpad/force-ibug-into-line into lp:launchpad.
=== modified file 'lib/lp/bugs/configure.zcml'
--- lib/lp/bugs/configure.zcml	2012-02-24 04:00:24 +0000
+++ lib/lp/bugs/configure.zcml	2012-03-08 05:47:22 +0000
@@ -716,171 +716,22 @@
 
         <class
             class="lp.bugs.model.bug.Bug">
-            <implements
-                interface="lp.bugs.interfaces.bug.IBug"/>
             <allow
-                attributes="
-                    id
-                    private
-                    userCanView"/>
+                interface="lp.bugs.interfaces.bug.IBugPublic"/>
             <require
                 permission="launchpad.View"
-                attributes="
-                    bugtasks
-                    default_bugtask
-                    affected_pillars
-                    permits_expiration
-                    can_expire
-                    subscriptions
-                    syncUpdate
-                    date_last_updated
-                    getActivityForDateRange
-                    getBugTask
-                    getDirectSubscribers
-                    getDirectSubscribersWithDetails
-                    getDirectSubscriptions
-                    getIndirectSubscribers
-                    is_complete
-                    maybeConfirmBugtasks
-                    official_tags
-                    who_made_private
-                    date_made_private
-                    userCanSetCommentVisibility
-                    personIsDirectSubscriber
-                    personIsAlsoNotifiedSubscriber
-                    personIsSubscribedToDuplicate
-                    shouldConfirmBugtasks
-                    heat
-                    heat_last_updated
-                    has_patches
-                    latest_patch
-                    latest_patch_uploaded
-                    datecreated
-                    name
-                    title
-                    description
-                    ownerID
-                    owner
-                    duplicateof
-                    communityscore
-                    communitytimestamp
-                    hits
-                    hitstimestamp
-                    activityscore
-                    activitytimestamp
-                    displayname
-                    activity
-                    initial_message
-                    linked_branches
-                    getVisibleLinkedBranches
-                    watches
-                    has_cves
-                    cves
-                    cve_links
-                    duplicates
-                    attachments
-                    attachments_unpopulated
-                    questions
-                    specifications
-                    followup_subject
-                    isSubscribed
-                    messages
-                    getBugNotificationRecipients
-                    hasBranch
-                    security_related
-                    tags
-                    getMessagesForView
-                    isSubscribedToDupes
-                    getSubscribersFromDuplicates
-                    getSubscriptionsFromDuplicates
-                    getSubscribersForPerson
-                    getSubscriptionForPerson
-                    getSubscriptionInfo
-                    indexed_messages
-                    _indexed_messages
-                    getAlsoNotifiedSubscribers
-                    getBugWatch
-                    canBeNominatedFor
-                    getNominationFor
-                    getNominations
-                    date_last_message
-                    number_of_duplicates
-                    message_count
-                    comment_count
-                    getQuestionCreatedFromBug
-                    canBeAQuestion
-                    getBugTasksByPackageName
-                    users_affected_count
-                    users_unaffected_count
-                    users_affected
-                    users_unaffected
-                    users_affected_count_with_dupes
-                    other_users_affected_count_with_dupes
-                    users_affected_with_dupes
-                    bug_messages
-                    isUserAffected
-                    getHWSubmissions
-                    isExpirable
-                    isMuted"/>
+                interface="lp.bugs.interfaces.bug.IBugView"/>
             <require
                 permission="launchpad.Edit"
-                attributes="
-                    addAttachment
-                    addChange
-                    addCommentNotification
-                    addNomination
-                    addTask
-                    addWatch
-                    convertToQuestion
-                    expireNotifications
-                    findCvesInText
-                    linkAttachment
-                    linkBranch
-                    linkCVE
-                    linkCVEAndReturnNothing
-                    linkHWSubmission
-                    linkMessage
-                    markAsDuplicate
-                    markUserAffected
-                    mute
-                    newMessage
-                    removeWatch
-                    setCommentVisibility
-                    setPrivate
-                    setSecurityRelated
-                    setPrivacyAndSecurityRelated
-                    setStatus
-                    subscribe
-                    unlinkBranch
-                    unlinkCVE
-                    unlinkHWSubmission
-                    unmute
-                    unsubscribe
-                    unsubscribeFromDupes
-                    updateHeat"
+                interface="lp.bugs.interfaces.bug.IBugEdit"
                 set_attributes="
-                    activity initial_message
-                    activityscore
-                    activitytimestamp
-                    communityscore
-                    communitytimestamp
-                    date_last_updated
-                    date_made_private
-                    datecreated
-                    hits
-                    hitstimestamp
-                    name
-                    owner
-                    ownerID
-                    security_related
-                    tags
-                    title description
-                    who_made_private"/>
+                    date_last_updated date_made_private datecreated
+                    name owner ownerID security_related tags title
+                    description who_made_private"/>
             <require
                 permission="launchpad.Admin"
-                attributes="
-                    setHeat"
-                set_attributes="heat_last_updated" />
+                interface="lp.bugs.interfaces.bug.IBugAdmin"
+                set_attributes="heat_last_updated"/>
         </class>
         <adapter
             for="lp.bugs.interfaces.bug.IBug"

=== modified file 'lib/lp/bugs/interfaces/bug.py'
--- lib/lp/bugs/interfaces/bug.py	2012-02-20 22:21:36 +0000
+++ lib/lp/bugs/interfaces/bug.py	2012-03-08 05:47:22 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 # pylint: disable-msg=E0211,E0213,E0602
@@ -12,10 +12,14 @@
     'CreatedBugWithNoBugTasksError',
     'IBug',
     'IBugAddForm',
+    'IBugAdmin',
     'IBugBecameQuestionEvent',
     'IBugDelta',
+    'IBugEdit',
     'IBugMute',
+    'IBugPublic',
     'IBugSet',
+    'IBugView',
     'IFileBugData',
     'IFrontPageBugAddForm',
     'IProjectGroupBugAddForm',
@@ -195,17 +199,25 @@
     return subject_field
 
 
-class IBug(IPrivacy, IHasLinkedBranches):
-    """The core bug entry."""
-    export_as_webservice_entry()
-
+class IBugPublic(IPrivacy):
+    """Public attributes for a Bug."""
     id = exported(
         Int(title=_('Bug ID'), required=True, readonly=True))
-    datecreated = exported(
-        Datetime(title=_('Date Created'), required=True, readonly=True),
-        exported_as='date_created')
-    date_last_updated = exported(
-        Datetime(title=_('Date Last Updated'), required=True, readonly=True))
+    # This is redefined from IPrivacy.private because the attribute is
+    # read-only. The value is guarded by setPrivate().
+    private = exported(
+        Bool(title=_("This bug report should be private"), required=False,
+             description=_("Private bug reports are visible only to "
+                           "their subscribers."),
+             default=False,
+             readonly=True))
+
+    def userCanView(user):
+        """Return True if `user` can see this IBug, false otherwise."""
+
+
+class IBugView(IHasLinkedBranches):
+    """IBug attributes that require launchpad.View permission."""
     name = exported(
         BugNameField(
             title=_('Nickname'), required=False,
@@ -225,37 +237,6 @@
     ownerID = Int(title=_('Owner'), required=True, readonly=True)
     owner = exported(
         Reference(IPerson, title=_("The owner's IPerson"), readonly=True))
-    duplicateof = exported(
-        DuplicateBug(title=_('Duplicate Of'), required=False, readonly=True),
-        exported_as='duplicate_of')
-    # This is redefined from IPrivacy.private because the attribute is
-    # read-only. The value is guarded by setPrivate().
-    private = exported(
-        Bool(title=_("This bug report should be private"), required=False,
-             description=_("Private bug reports are visible only to "
-                           "their subscribers."),
-             default=False,
-             readonly=True))
-    date_made_private = exported(
-        Datetime(title=_('Date Made Private'), required=False, readonly=True))
-    who_made_private = exported(
-        PublicPersonChoice(
-            title=_('Who Made Private'), required=False,
-            vocabulary='ValidPersonOrTeam',
-            description=_("The person who set this bug private."),
-            readonly=True))
-    security_related = exported(
-        Bool(title=_("This bug is a security vulnerability."),
-             required=False, default=False, readonly=True))
-    displayname = TextLine(title=_("Text of the form 'Bug #X"),
-        readonly=True)
-    activity = exported(
-        doNotSnapshot(CollectionField(
-            title=_('Log of activity that has occurred on this bug.'),
-            value_type=Reference(schema=IBugActivity),
-            readonly=True)))
-    initial_message = Attribute(
-        "The message that was specified when creating the bug")
     bugtasks = exported(
         CollectionField(
             title=_('BugTasks on this bug, sorted upstream, then '
@@ -266,8 +247,62 @@
     default_bugtask = Reference(
         title=_("The first bug task to have been filed."),
         schema=IBugTask)
+    duplicateof = exported(
+        DuplicateBug(title=_('Duplicate Of'), required=False, readonly=True),
+        exported_as='duplicate_of')
+    datecreated = exported(
+        Datetime(title=_('Date Created'), required=True, readonly=True),
+        exported_as='date_created')
+    displayname = TextLine(title=_("Text of the form 'Bug #X"),
+        readonly=True)
+    activity = exported(
+        doNotSnapshot(CollectionField(
+            title=_('Log of activity that has occurred on this bug.'),
+            value_type=Reference(schema=IBugActivity),
+            readonly=True)))
     affected_pillars = Attribute(
         'The "pillars", products or distributions, affected by this bug.')
+    permits_expiration = Bool(
+        title=_("Does the bug's state permit expiration?"),
+        description=_(
+            "Expiration is permitted when the bug is not valid anywhere, "
+            "a message was sent to the bug reporter, and the bug is "
+            "associated with pillars that have enabled bug expiration."),
+        readonly=True)
+    can_expire = exported(
+        Bool(
+            title=_("Can the Incomplete bug expire? "
+                "Expiration may happen when the bug permits expiration, "
+                "and a bugtask cannot be confirmed."),
+            readonly=True),
+        ('devel', dict(exported=False)), exported=True)
+    subscriptions = exported(
+        doNotSnapshot(CollectionField(
+            title=_('Subscriptions'),
+            value_type=Reference(schema=Interface),
+            readonly=True)))
+    date_last_updated = exported(
+        Datetime(title=_('Date Last Updated'), required=True, readonly=True))
+    is_complete = Bool(
+        title=_("Is Complete?"),
+        description=_(
+            "True or False depending on whether this bug is considered "
+            "completely addressed. A bug in Launchpad is completely "
+            "addressed when there are no tasks that are still open for "
+            "the bug."),
+        readonly=True)
+    official_tags = Attribute("The official bug tags relevant to this bug.")
+    who_made_private = exported(
+        PublicPersonChoice(
+            title=_('Who Made Private'), required=False,
+            vocabulary='ValidPersonOrTeam',
+            description=_("The person who set this bug private."),
+            readonly=True))
+    date_made_private = exported(
+        Datetime(title=_('Date Made Private'), required=False, readonly=True))
+    heat = exported(
+        Int(title=_("The 'heat' of the bug"),
+        required=False, readonly=True))
     watches = exported(
         CollectionField(
             title=_("All bug watches associated with this bug."),
@@ -281,11 +316,6 @@
             readonly=True))
     has_cves = Bool(title=u"True if the bug has cve entries.")
     cve_links = Attribute('Links between this bug and CVE entries.')
-    subscriptions = exported(
-        doNotSnapshot(CollectionField(
-            title=_('Subscriptions'),
-            value_type=Reference(schema=Interface),
-            readonly=True)))
     duplicates = exported(
         CollectionField(
             title=_("MultiJoin of bugs which are dupes of this one."),
@@ -305,6 +335,17 @@
             title=_("List of bug attachments."),
             value_type=Reference(schema=IBugAttachment),
             readonly=True)))
+    security_related = exported(
+        Bool(title=_("This bug is a security vulnerability."),
+             required=False, default=False, readonly=True))
+    has_patches = Attribute("Does this bug have any patches?")
+    latest_patch_uploaded = exported(
+        Datetime(
+            title=_('Date when the most recent patch was uploaded.'),
+            required=False, readonly=True))
+    latest_patch = Attribute("The most recent patch of this bug.")
+    initial_message = Attribute(
+        "The message that was specified when creating the bug")
     questions = Attribute("List of questions related to this bug.")
     specifications = Attribute("List of related specifications.")
     linked_branches = exported(
@@ -318,28 +359,12 @@
         description=_("Space-separated keywords for classifying "
             "this bug report."),
             value_type=Tag(), required=False))
-    is_complete = Bool(
-        title=_("Is Complete?"),
-        description=_(
-            "True or False depending on whether this bug is considered "
-            "completely addressed. A bug in Launchpad is completely "
-            "addressed when there are no tasks that are still open for "
-            "the bug."),
-        readonly=True)
-    permits_expiration = Bool(
-        title=_("Does the bug's state permit expiration?"),
-        description=_(
-            "Expiration is permitted when the bug is not valid anywhere, "
-            "a message was sent to the bug reporter, and the bug is "
-            "associated with pillars that have enabled bug expiration."),
-        readonly=True)
-    can_expire = exported(
-        Bool(
-            title=_("Can the Incomplete bug expire? "
-                "Expiration may happen when the bug permits expiration, "
-                "and a bugtask cannot be confirmed."),
-            readonly=True),
-        ('devel', dict(exported=False)), exported=True)
+    messages = doNotSnapshot(CollectionField(
+            title=_("The messages related to this object, in reverse "
+                    "order of creation (so newest first)."),
+            readonly=True,
+            value_type=Reference(schema=IMessage)))
+    followup_subject = Attribute("The likely subject of the next message.")
     date_last_message = exported(
         Datetime(title=_("Date of last bug message"),
                  required=False, readonly=True))
@@ -384,26 +409,12 @@
         title=_('Users affected (including duplicates)'),
         value_type=Reference(schema=IPerson),
         readonly=True)))
-
-    heat = exported(
-        Int(title=_("The 'heat' of the bug"),
-        required=False, readonly=True))
-    heat_last_updated = Datetime(
-        title=_('Heat Last Updated'), required=False, readonly=True)
-
     # Adding related BugMessages provides a hook for getting at
     # BugMessage.message.visible when building bug comments.
     bug_messages = Attribute('The bug messages related to this object.')
     comment_count = Attribute(
         "The number of comments on this bug, not including the initial "
         "comment.")
-
-    messages = doNotSnapshot(CollectionField(
-            title=_("The messages related to this object, in reverse "
-                    "order of creation (so newest first)."),
-            readonly=True,
-            value_type=Reference(schema=IMessage)))
-
     indexed_messages = doNotSnapshot(exported(
         CollectionField(
             title=_("The messages related to this object, in reverse "
@@ -422,18 +433,8 @@
             None to prevent lazy evaluation triggering database lookups.
         """
 
-    followup_subject = Attribute("The likely subject of the next message.")
-
-    has_patches = Attribute("Does this bug have any patches?")
-
-    latest_patch_uploaded = exported(
-        Datetime(
-            title=_('Date when the most recent patch was uploaded.'),
-            required=False, readonly=True))
-
-    latest_patch = Attribute("The most recent patch of this bug.")
-
-    official_tags = Attribute("The official bug tags relevant to this bug.")
+    def hasBranch(branch):
+        """Is this branch linked to this bug?"""
 
     @accessor_for(linked_branches)
     @call_with(user=REQUEST_USER)
@@ -442,48 +443,6 @@
     def getVisibleLinkedBranches(user):
         """Rertun the linked to this bug that are visible by `user`."""
 
-    @operation_parameters(
-        subject=optional_message_subject_field(),
-        content=copy_field(IMessage['content']))
-    @call_with(owner=REQUEST_USER)
-    @export_factory_operation(IMessage, [])
-    def newMessage(owner, subject, content):
-        """Create a new message, and link it to this object."""
-
-    # The level actually uses BugNotificationLevel as its vocabulary,
-    # but due to circular import problems we fix that in
-    # _schema_circular_imports.py rather than here.
-    @operation_parameters(
-        person=Reference(IPerson, title=_('Person'), required=True),
-        level=Choice(
-            vocabulary=DBEnumeratedType, required=False,
-            title=_('Level')))
-    @call_with(subscribed_by=REQUEST_USER, suppress_notify=False)
-    @export_write_operation()
-    def subscribe(person, subscribed_by, suppress_notify=True, level=None):
-        """Subscribe `person` to the bug.
-
-        :param person: the subscriber.
-        :param subscribed_by: the person who created the subscription.
-        :param suppress_notify: a flag to suppress notify call.
-        :param level: The BugNotificationLevel for the new subscription.
-        :return: an `IBugSubscription`.
-        """
-
-    @operation_parameters(
-        person=Reference(IPerson, title=_('Person'), required=False))
-    @call_with(unsubscribed_by=REQUEST_USER)
-    @export_write_operation()
-    def unsubscribe(person, unsubscribed_by):
-        """Remove this person's subscription to this bug."""
-
-    @operation_parameters(
-        person=Reference(IPerson, title=_('Person'), required=False))
-    @call_with(unsubscribed_by=REQUEST_USER)
-    @export_write_operation()
-    def unsubscribeFromDupes(person, unsubscribed_by):
-        """Remove this person's subscription from all dupes of this bug."""
-
     def isSubscribed(person):
         """Is person subscribed to this bug?
 
@@ -506,24 +465,6 @@
         :returns: True if the user has muted all email from this bug.
         """
 
-    @operation_parameters(
-        person=Reference(IPerson, title=_('Person'), required=False))
-    @call_with(muted_by=REQUEST_USER)
-    @export_write_operation()
-    @operation_for_version('devel')
-    def mute(person, muted_by):
-        """Add a muted subscription for `person`."""
-
-    @operation_parameters(
-        person=Reference(IPerson, title=_('Person'), required=False))
-    @call_with(unmuted_by=REQUEST_USER)
-    @export_write_operation()
-    @operation_for_version('devel')
-    def unmute(person, unmuted_by):
-        """Remove a muted subscription for `person`.
-
-        Returns previously muted direct subscription, if any."""
-
     def getDirectSubscriptions():
         """A sequence of IBugSubscriptions directly linked to this bug."""
 
@@ -597,118 +538,6 @@
         True to include the master bug's subscribers as recipients.
         """
 
-    def addCommentNotification(message, recipients=None, activity=None):
-        """Add a bug comment notification.
-
-        If a BugActivity instance is provided as an `activity`, it is linked
-        to the notification."""
-
-    def addChange(change, recipients=None):
-        """Record a change to the bug.
-
-        :param change: An `IBugChange` instance from which to take the
-            change data.
-        :param recipients: A set of `IBugNotificationRecipient`s to whom
-            to send notifications about this change. If None is passed
-            the default list of recipients for the bug will be used.
-        """
-
-    def expireNotifications():
-        """Expire any pending notifications that have not been emailed.
-
-        This will mark any notifications related to this bug as having
-        been emailed.  The intent is to prevent large quantities of
-        bug mail being generated during bulk imports or changes.
-        """
-
-    @call_with(owner=REQUEST_USER)
-    @rename_parameters_as(
-        bugtracker='bug_tracker', remotebug='remote_bug')
-    @export_factory_operation(
-        IBugWatch, ['bugtracker', 'remotebug'])
-    def addWatch(bugtracker, remotebug, owner):
-        """Create a new watch for this bug on the given remote bug and bug
-        tracker, owned by the person given as the owner.
-        """
-
-    def removeWatch(bug_watch, owner):
-        """Remove a bug watch from the bug."""
-
-    @call_with(owner=REQUEST_USER)
-    @operation_parameters(target=copy_field(IBugTask['target']))
-    @export_factory_operation(IBugTask, [])
-    def addTask(owner, target):
-        """Create a new bug task on this bug.
-
-        :raises IllegalTarget: if the bug task cannot be added to the bug.
-        """
-
-    def hasBranch(branch):
-        """Is this branch linked to this bug?"""
-
-    @call_with(owner=REQUEST_USER)
-    @operation_parameters(
-        data=Bytes(constraint=attachment_size_constraint),
-        comment=Text(), filename=TextLine(), is_patch=Bool(),
-        content_type=TextLine(), description=Text())
-    @export_factory_operation(IBugAttachment, [])
-    def addAttachment(owner, data, comment, filename, is_patch=False,
-                      content_type=None, description=None):
-        """Attach a file to this bug.
-
-        :owner: An IPerson.
-        :data: A file-like object, or a `str`.
-        :description: A brief description of the attachment.
-        :comment: An IMessage or string.
-        :filename: A string.
-        :is_patch: A boolean.
-        """
-
-    def linkAttachment(owner, file_alias, comment, is_patch=False,
-                       description=None):
-        """Link an `ILibraryFileAlias` to this bug.
-
-        :owner: An IPerson.
-        :file_alias: The `ILibraryFileAlias` to link to this bug.
-        :description: A brief description of the attachment.
-        :comment: An IMessage or string.
-        :is_patch: A boolean.
-
-        This method should only be called by addAttachment() and
-        FileBugViewBase.submit_bug_action, otherwise
-        we may get inconsistent settings of bug.private and
-        file_alias.restricted.
-        """
-
-    def linkCVE(cve, user):
-        """Ensure that this CVE is linked to this bug."""
-
-    # XXX intellectronica 2008-11-06 Bug #294858:
-    # We use this method to suppress the return value
-    # from linkCVE, which we don't want to export.
-    # In the future we'll have a decorator which does that for us.
-    @call_with(user=REQUEST_USER)
-    @operation_parameters(cve=Reference(ICve, title=_('CVE'), required=True))
-    @export_operation_as('linkCVE')
-    @export_write_operation()
-    def linkCVEAndReturnNothing(cve, user):
-        """Ensure that this CVE is linked to this bug."""
-
-    @call_with(user=REQUEST_USER)
-    @operation_parameters(cve=Reference(ICve, title=_('CVE'), required=True))
-    @export_write_operation()
-    def unlinkCVE(cve, user):
-        """Ensure that any links between this bug and the given CVE are
-        removed.
-        """
-
-    def findCvesInText(text, user):
-        """Find any CVE references in the given text, make sure they exist
-        in the database, and are linked to this bug.
-
-        The user is the one linking to the CVE.
-        """
-
     def canBeAQuestion():
         """Return True of False if a question can be created from this bug.
 
@@ -719,39 +548,9 @@
         3. The bug was not made into a question previously.
         """
 
-    def convertToQuestion(person, comment=None):
-        """Create and return a Question from this Bug.
-
-        Bugs that are also in external bug trackers cannot be converted
-        to questions. This is also true for bugs that are being developed.
-
-        The `IQuestionTarget` is provided by the `IBugTask` that is not
-        Invalid and is not a conjoined slave. Only one question can be
-        made from a bug.
-
-        An AssertionError is raised if the bug has zero or many BugTasks
-        that can provide a QuestionTarget. It will also be raised if a
-        question was previously created from the bug.
-
-        :person: The `IPerson` creating a question from this bug
-        :comment: A string. An explanation of why the bug is a question.
-        """
-
     def getQuestionCreatedFromBug():
         """Return the question created from this Bug, or None."""
 
-    def linkMessage(message, bugwatch=None, user=None,
-                    remote_comment_id=None):
-        """Add a comment to this bug.
-
-            :param message: The `IMessage` to be used as a comment.
-            :param bugwatch: The `IBugWatch` of the bug this comment was
-                imported from, if it's an imported comment.
-            :param user: The `IPerson` adding the comment.
-            :param remote_comment_id: The id this comment has in the
-                remote bug tracker, if it's an imported comment.
-        """
-
     def getMessagesForView(slice_info):
         """Return BugMessage,Message,MessageChunks for renderinger.
 
@@ -764,20 +563,6 @@
 
     @operation_parameters(
         target=Reference(schema=Interface, title=_('Target')))
-    @call_with(owner=REQUEST_USER)
-    @export_factory_operation(Interface, [])
-    def addNomination(owner, target):
-        """Nominate a bug for an IDistroSeries or IProductSeries.
-
-        :owner: An IPerson.
-        :target: An IDistroSeries or IProductSeries.
-
-        This method creates and returns a BugNomination. (See
-        lp.bugs.model.bugnomination.BugNomination.)
-        """
-
-    @operation_parameters(
-        target=Reference(schema=Interface, title=_('Target')))
     @export_read_operation()
     def canBeNominatedFor(target):
         """Can this bug nominated for this target?
@@ -827,66 +612,6 @@
         Return None if this bug doesn't have such a bug watch.
         """
 
-    def setStatus(target, status, user):
-        """Set the status of the bugtask related to the specified target.
-
-            :target: The target of the bugtask that should be modified.
-            :status: The status the bugtask should be set to.
-            :user: The `IPerson` doing the change.
-
-        If a bug task was edited, emit a
-        `lazr.lifecycle.interfaces.IObjectModifiedEvent` and
-        return the edited bugtask.
-
-        Return None if no bugtask was edited.
-        """
-
-    @mutator_for(private)
-    @operation_parameters(private=copy_field(private))
-    @call_with(who=REQUEST_USER)
-    @export_write_operation()
-    def setPrivate(private, who):
-        """Set bug privacy.
-
-            :private: True/False.
-            :who: The IPerson who is making the change.
-
-        Return True if a change is made, False otherwise.
-        """
-
-    @mutator_for(security_related)
-    @operation_parameters(security_related=copy_field(security_related))
-    @call_with(who=REQUEST_USER)
-    @export_write_operation()
-    def setSecurityRelated(security_related, who):
-        """Set bug security.
-
-            :security_related: True/False.
-            :who: The IPerson who is making the change.
-
-        This may also cause the security contact to be subscribed
-        if one is registered and if the bug is not private.
-
-        Return True if a change is made, False otherwise.
-        """
-
-    @operation_parameters(
-        private=copy_field(private),
-        security_related=copy_field(security_related),
-        )
-    @call_with(who=REQUEST_USER)
-    @export_write_operation()
-    @operation_for_version("devel")
-    def setPrivacyAndSecurityRelated(private, security_related, who):
-        """Set bug privacy and security .
-
-            :private: True/False.
-            :security_related: True/False.
-            :who: The IPerson who is making the change.
-
-        Return (private_changed, security_related_changed) tuple.
-        """
-
     def getBugTask(target):
         """Return the bugtask with the specified target.
 
@@ -912,37 +637,6 @@
     def isUserAffected(user):
         """Is :user: marked as affected by this bug?"""
 
-    @operation_parameters(
-        affected=Bool(
-            title=_("Does this bug affect you?"),
-            required=False, default=True))
-    @call_with(user=REQUEST_USER)
-    @export_write_operation()
-    def markUserAffected(user, affected=True):
-        """Mark :user: as affected by this bug."""
-
-    @mutator_for(duplicateof)
-    @operation_parameters(duplicate_of=copy_field(duplicateof))
-    @export_write_operation()
-    def markAsDuplicate(duplicate_of):
-        """Mark this bug as a duplicate of another."""
-
-    @operation_parameters(
-        comment_number=Int(
-            title=_('The number of the comment in the list of messages.'),
-            required=True),
-        visible=Bool(title=_('Show this comment?'), required=True))
-    @call_with(user=REQUEST_USER)
-    @export_write_operation()
-    def setCommentVisibility(user, comment_number, visible):
-        """Set the visible attribute on a bug comment.  This is restricted
-        to Launchpad admins, and will return a HTTP Error 401: Unauthorized
-        error for non-admin callers.
-        """
-
-    def userCanView(user):
-        """Return True if `user` can see this IBug, false otherwise."""
-
     def userCanSetCommentVisibility(user):
         """Return True if `user` can set bug comment visibility.
 
@@ -962,20 +656,6 @@
 
         """
 
-    @operation_parameters(
-        submission=Reference(
-            Interface, title=_('A HWDB submission'), required=True))
-    @export_write_operation()
-    def linkHWSubmission(submission):
-        """Link a `HWSubmission` to this bug."""
-
-    @operation_parameters(
-        submission=Reference(
-            Interface, title=_('A HWDB submission'), required=True))
-    @export_write_operation()
-    def unlinkHWSubmission(submission):
-        """Remove a link to a `HWSubmission`."""
-
     @call_with(user=REQUEST_USER)
     @operation_returns_collection_of(Interface)
     @export_read_operation()
@@ -990,12 +670,6 @@
         if the user is the owner or an admin.
         """
 
-    def setHeat(heat, timestamp=None):
-        """Set the heat for the bug."""
-
-    def updateHeat():
-        """Update the heat for the bug."""
-
     @operation_parameters(
         days_old=Int(
             title=_('Number of days of inactivity for which to check.'),
@@ -1019,6 +693,344 @@
         :param end_date: The latest date for which activity can be
             returned.
         """
+    
+    def maybeConfirmBugtasks():
+        """Maybe try to confirm our new bugtasks."""
+
+
+class IBugEdit(Interface):
+    @call_with(owner=REQUEST_USER)
+    @operation_parameters(
+        data=Bytes(constraint=attachment_size_constraint),
+        comment=Text(), filename=TextLine(), is_patch=Bool(),
+        content_type=TextLine(), description=Text())
+    @export_factory_operation(IBugAttachment, [])
+    def addAttachment(owner, data, comment, filename, is_patch=False,
+                      content_type=None, description=None):
+        """Attach a file to this bug.
+
+        :owner: An IPerson.
+        :data: A file-like object, or a `str`.
+        :description: A brief description of the attachment.
+        :comment: An IMessage or string.
+        :filename: A string.
+        :is_patch: A boolean.
+        """
+
+    def addCommentNotification(message, recipients=None, activity=None):
+        """Add a bug comment notification.
+
+        If a BugActivity instance is provided as an `activity`, it is linked
+        to the notification."""
+
+    def addChange(change, recipients=None):
+        """Record a change to the bug.
+
+        :param change: An `IBugChange` instance from which to take the
+            change data.
+        :param recipients: A set of `IBugNotificationRecipient`s to whom
+            to send notifications about this change. If None is passed
+            the default list of recipients for the bug will be used.
+        """
+
+    @operation_parameters(
+        target=Reference(schema=Interface, title=_('Target')))
+    @call_with(owner=REQUEST_USER)
+    @export_factory_operation(Interface, [])
+    def addNomination(owner, target):
+        """Nominate a bug for an IDistroSeries or IProductSeries.
+
+        :owner: An IPerson.
+        :target: An IDistroSeries or IProductSeries.
+
+        This method creates and returns a BugNomination. (See
+        lp.bugs.model.bugnomination.BugNomination.)
+        """
+
+    @call_with(owner=REQUEST_USER)
+    @rename_parameters_as(
+        bugtracker='bug_tracker', remotebug='remote_bug')
+    @export_factory_operation(
+        IBugWatch, ['bugtracker', 'remotebug'])
+    def addWatch(bugtracker, remotebug, owner):
+        """Create a new watch for this bug on the given remote bug and bug
+        tracker, owned by the person given as the owner.
+        """
+
+    def removeWatch(bug_watch, owner):
+        """Remove a bug watch from the bug."""
+
+    @call_with(owner=REQUEST_USER)
+    @operation_parameters(target=copy_field(IBugTask['target']))
+    @export_factory_operation(IBugTask, [])
+    def addTask(owner, target):
+        """Create a new bug task on this bug.
+
+        :raises IllegalTarget: if the bug task cannot be added to the bug.
+        """
+
+    def convertToQuestion(person, comment=None):
+        """Create and return a Question from this Bug.
+
+        Bugs that are also in external bug trackers cannot be converted
+        to questions. This is also true for bugs that are being developed.
+
+        The `IQuestionTarget` is provided by the `IBugTask` that is not
+        Invalid and is not a conjoined slave. Only one question can be
+        made from a bug.
+
+        An AssertionError is raised if the bug has zero or many BugTasks
+        that can provide a QuestionTarget. It will also be raised if a
+        question was previously created from the bug.
+
+        :person: The `IPerson` creating a question from this bug
+        :comment: A string. An explanation of why the bug is a question.
+        """
+
+    def expireNotifications():
+        """Expire any pending notifications that have not been emailed.
+
+        This will mark any notifications related to this bug as having
+        been emailed.  The intent is to prevent large quantities of
+        bug mail being generated during bulk imports or changes.
+        """
+
+    def findCvesInText(text, user):
+        """Find any CVE references in the given text, make sure they exist
+        in the database, and are linked to this bug.
+
+        The user is the one linking to the CVE.
+        """
+
+    def linkAttachment(owner, file_alias, comment, is_patch=False,
+                       description=None):
+        """Link an `ILibraryFileAlias` to this bug.
+
+        :owner: An IPerson.
+        :file_alias: The `ILibraryFileAlias` to link to this bug.
+        :description: A brief description of the attachment.
+        :comment: An IMessage or string.
+        :is_patch: A boolean.
+
+        This method should only be called by addAttachment() and
+        FileBugViewBase.submit_bug_action, otherwise
+        we may get inconsistent settings of bug.private and
+        file_alias.restricted.
+        """
+
+    def linkCVE(cve, user):
+        """Ensure that this CVE is linked to this bug."""
+
+    # XXX intellectronica 2008-11-06 Bug #294858:
+    # We use this method to suppress the return value
+    # from linkCVE, which we don't want to export.
+    # In the future we'll have a decorator which does that for us.
+    @call_with(user=REQUEST_USER)
+    @operation_parameters(cve=Reference(ICve, title=_('CVE'), required=True))
+    @export_operation_as('linkCVE')
+    @export_write_operation()
+    def linkCVEAndReturnNothing(cve, user):
+        """Ensure that this CVE is linked to this bug."""
+
+    @call_with(user=REQUEST_USER)
+    @operation_parameters(cve=Reference(ICve, title=_('CVE'), required=True))
+    @export_write_operation()
+    def unlinkCVE(cve, user):
+        """Ensure that any links between this bug and the given CVE are
+        removed.
+        """
+
+    @mutator_for(IBugPublic['private'])
+    @operation_parameters(private=copy_field(IBugPublic['private']))
+    @call_with(who=REQUEST_USER)
+    @export_write_operation()
+    def setPrivate(private, who):
+        """Set bug privacy.
+
+            :private: True/False.
+            :who: The IPerson who is making the change.
+
+        Return True if a change is made, False otherwise.
+        """
+
+    @mutator_for(IBugView['security_related'])
+    @operation_parameters(
+        security_related=copy_field(IBugView['security_related']))
+    @call_with(who=REQUEST_USER)
+    @export_write_operation()
+    def setSecurityRelated(security_related, who):
+        """Set bug security.
+
+            :security_related: True/False.
+            :who: The IPerson who is making the change.
+
+        This may also cause the security contact to be subscribed
+        if one is registered and if the bug is not private.
+
+        Return True if a change is made, False otherwise.
+        """
+
+    @operation_parameters(
+        private=copy_field(IBugPublic['private']),
+        security_related=copy_field(IBugView['security_related']),
+        )
+    @call_with(who=REQUEST_USER)
+    @export_write_operation()
+    @operation_for_version("devel")
+    def setPrivacyAndSecurityRelated(private, security_related, who):
+        """Set bug privacy and security .
+
+            :private: True/False.
+            :security_related: True/False.
+            :who: The IPerson who is making the change.
+
+        Return (private_changed, security_related_changed) tuple.
+        """
+
+    @operation_parameters(
+        submission=Reference(
+            Interface, title=_('A HWDB submission'), required=True))
+    @export_write_operation()
+    def linkHWSubmission(submission):
+        """Link a `HWSubmission` to this bug."""
+
+    @operation_parameters(
+        submission=Reference(
+            Interface, title=_('A HWDB submission'), required=True))
+    @export_write_operation()
+    def unlinkHWSubmission(submission):
+        """Remove a link to a `HWSubmission`."""
+
+    def linkMessage(message, bugwatch=None, user=None,
+                    remote_comment_id=None):
+        """Add a comment to this bug.
+
+            :param message: The `IMessage` to be used as a comment.
+            :param bugwatch: The `IBugWatch` of the bug this comment was
+                imported from, if it's an imported comment.
+            :param user: The `IPerson` adding the comment.
+            :param remote_comment_id: The id this comment has in the
+                remote bug tracker, if it's an imported comment.
+        """
+
+    @operation_parameters(
+        affected=Bool(
+            title=_("Does this bug affect you?"),
+            required=False, default=True))
+    @call_with(user=REQUEST_USER)
+    @export_write_operation()
+    def markUserAffected(user, affected=True):
+        """Mark :user: as affected by this bug."""
+
+    @mutator_for(IBugView['duplicateof'])
+    @operation_parameters(duplicate_of=copy_field(IBugView['duplicateof']))
+    @export_write_operation()
+    def markAsDuplicate(duplicate_of):
+        """Mark this bug as a duplicate of another."""
+
+    @operation_parameters(
+        comment_number=Int(
+            title=_('The number of the comment in the list of messages.'),
+            required=True),
+        visible=Bool(title=_('Show this comment?'), required=True))
+    @call_with(user=REQUEST_USER)
+    @export_write_operation()
+    def setCommentVisibility(user, comment_number, visible):
+        """Set the visible attribute on a bug comment.  This is restricted
+        to Launchpad admins, and will return a HTTP Error 401: Unauthorized
+        error for non-admin callers.
+        """
+
+    @operation_parameters(
+        person=Reference(IPerson, title=_('Person'), required=False))
+    @call_with(muted_by=REQUEST_USER)
+    @export_write_operation()
+    @operation_for_version('devel')
+    def mute(person, muted_by):
+        """Add a muted subscription for `person`."""
+
+    @operation_parameters(
+        person=Reference(IPerson, title=_('Person'), required=False))
+    @call_with(unmuted_by=REQUEST_USER)
+    @export_write_operation()
+    @operation_for_version('devel')
+    def unmute(person, unmuted_by):
+        """Remove a muted subscription for `person`.
+
+        Returns previously muted direct subscription, if any."""
+
+    @operation_parameters(
+        subject=optional_message_subject_field(),
+        content=copy_field(IMessage['content']))
+    @call_with(owner=REQUEST_USER)
+    @export_factory_operation(IMessage, [])
+    def newMessage(owner, subject, content):
+        """Create a new message, and link it to this object."""
+
+    # The level actually uses BugNotificationLevel as its vocabulary,
+    # but due to circular import problems we fix that in
+    # _schema_circular_imports.py rather than here.
+    @operation_parameters(
+        person=Reference(IPerson, title=_('Person'), required=True),
+        level=Choice(
+            vocabulary=DBEnumeratedType, required=False,
+            title=_('Level')))
+    @call_with(subscribed_by=REQUEST_USER, suppress_notify=False)
+    @export_write_operation()
+    def subscribe(person, subscribed_by, suppress_notify=True, level=None):
+        """Subscribe `person` to the bug.
+
+        :param person: the subscriber.
+        :param subscribed_by: the person who created the subscription.
+        :param suppress_notify: a flag to suppress notify call.
+        :param level: The BugNotificationLevel for the new subscription.
+        :return: an `IBugSubscription`.
+        """
+
+    @operation_parameters(
+        person=Reference(IPerson, title=_('Person'), required=False))
+    @call_with(unsubscribed_by=REQUEST_USER)
+    @export_write_operation()
+    def unsubscribe(person, unsubscribed_by):
+        """Remove this person's subscription to this bug."""
+
+    @operation_parameters(
+        person=Reference(IPerson, title=_('Person'), required=False))
+    @call_with(unsubscribed_by=REQUEST_USER)
+    @export_write_operation()
+    def unsubscribeFromDupes(person, unsubscribed_by):
+        """Remove this person's subscription from all dupes of this bug."""
+
+    def setStatus(target, status, user):
+        """Set the status of the bugtask related to the specified target.
+
+            :target: The target of the bugtask that should be modified.
+            :status: The status the bugtask should be set to.
+            :user: The `IPerson` doing the change.
+
+        If a bug task was edited, emit a
+        `lazr.lifecycle.interfaces.IObjectModifiedEvent` and
+        return the edited bugtask.
+
+        Return None if no bugtask was edited.
+        """
+
+    def updateHeat():
+        """Update the heat for the bug."""
+
+
+class IBugAdmin(Interface):
+    heat_last_updated = Datetime(
+        title=_('Heat Last Updated'), required=False, readonly=True)
+
+    def setHeat(heat, timestamp=None):
+        """Set the heat for the bug."""
+
+
+
+class IBug(IBugPublic, IBugView, IBugEdit, IBugAdmin):
+    """The core bug entry."""
+    export_as_webservice_entry()
 
 
 # We are forced to define these now to avoid circular import problems.

=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py	2012-03-07 12:58:06 +0000
+++ lib/lp/bugs/model/bug.py	2012-03-08 05:47:22 +0000
@@ -1,4 +1,4 @@
-# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
+# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 # pylint: disable-msg=E0611,W0212
@@ -2006,7 +2006,7 @@
         return self.users_affected_count_with_dupes > 1
 
     def maybeConfirmBugtasks(self):
-        """Maybe try to confirm our new bugtasks."""
+        """See `IBug`."""
         if self.shouldConfirmBugtasks():
             for bugtask in self.bugtasks:
                 bugtask.maybeConfirm()