~launchpad-pqm/launchpad/devel

14538.2.49 by Curtis Hovey
Updated copyright.
1
# Copyright 2010-2011 Canonical Ltd.  This software is licensed under the
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
2
# GNU Affero General Public License version 3 (see the file LICENSE).
3
4
"""Classes and logic for the checkwatches BugWatchUpdater."""
5
6
__metaclass__ = type
10694.2.35 by Graham Binns
Fixed comment pushing and importing tests.
7
__all__ = [
8
    'BugWatchUpdater',
9
    ]
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
10
11
import sys
12
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
13
from lazr.lifecycle.event import ObjectCreatedEvent
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
14
from zope.component import getUtility
15
from zope.event import notify
16
17
from canonical.launchpad.helpers import get_email_template
18
from canonical.launchpad.webapp.publisher import canonical_url
11270.1.3 by Tim Penhey
Changed NotFoundError imports - gee there were a lot of them.
19
from lp.app.errors import NotFoundError
13130.1.12 by Curtis Hovey
Sorted imports.
20
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
12599.4.2 by Leonard Richardson
Merge from trunk.
21
from lp.bugs.externalbugtracker.base import BugWatchUpdateError
10694.2.44 by Graham Binns
Fixed lint.
22
from lp.bugs.interfaces.bug import IBugSet
10694.5.23 by Graham Binns
Errors in comment imports are now recorded correctly.
23
from lp.bugs.interfaces.bugwatch import BugWatchActivityStatus
10694.2.32 by Graham Binns
Err... Everything seems to work. That's worrying.
24
from lp.bugs.scripts.checkwatches.base import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
25
    commit_before,
26
    WorkingBase,
27
    )
10694.5.2 by Graham Binns
Much refactoring and cleaning up of lint.
28
from lp.bugs.scripts.checkwatches.utilities import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
29
    get_remote_system_oops_properties,
30
    )
10694.2.44 by Graham Binns
Fixed lint.
31
from lp.registry.interfaces.person import PersonCreationRationale
13130.1.12 by Curtis Hovey
Sorted imports.
32
from lp.services.messages.interfaces.message import IMessageSet
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
33
34
10694.2.32 by Graham Binns
Err... Everything seems to work. That's worrying.
35
class BugWatchUpdater(WorkingBase):
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
36
    """Handles the updating of a single BugWatch for checkwatches."""
37
10694.2.39 by Graham Binns
Applied allenap's patch for initFromParent().
38
    def __init__(self, parent, bug_watch, external_bugtracker):
39
        self.initFromParent(parent)
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
40
        self.bug_watch = bug_watch
10694.2.31 by Graham Binns
Started trying to get tests working. Need to get the WorkingBase stuff playing nice.
41
        self.external_bugtracker = external_bugtracker
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
42
10694.5.23 by Graham Binns
Errors in comment imports are now recorded correctly.
43
        # We save these for the sake of error reporting.
44
        self.remote_bug = self.bug_watch.remotebug
45
        self.local_bug = self.bug_watch.bug.id
46
        self.oops_properties = get_remote_system_oops_properties(
47
            self.external_bugtracker)
48
        self.oops_properties.extend([
49
            ('URL', self.bug_watch.url),
50
            ('bug_id', self.remote_bug),
51
            ('local_ids', str(self.local_bug))])
52
10694.5.28 by Graham Binns
Added a test to ensure that can_back_link, etc., are inherited.
53
        self.can_import_comments = parent.can_import_comments
54
        self.can_push_comments = parent.can_push_comments
55
        self.can_back_link = parent.can_back_link
56
10694.2.32 by Graham Binns
Err... Everything seems to work. That's worrying.
57
    @commit_before
10694.2.29 by Graham Binns
Fixed lint.
58
    def updateBugWatch(self, new_remote_status, new_malone_status,
12060.4.1 by Graham Binns
Removed unnecessary parameters for updateBugWatch().
59
                       new_remote_importance, new_malone_importance):
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
60
        """Update the BugWatch."""
61
        with self.transaction:
62
            if new_malone_status is not None:
10694.2.29 by Graham Binns
Fixed lint.
63
                self.bug_watch.updateStatus(
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
64
                    new_remote_status, new_malone_status)
65
            if new_malone_importance is not None:
10694.2.29 by Graham Binns
Fixed lint.
66
                self.bug_watch.updateImportance(
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
67
                    new_remote_importance, new_malone_importance)
10694.5.18 by Graham Binns
Removed the (now unnecessary) oops_id and error params from BugWatchUpdater.updateBugWatches().
68
            # Only sync comments and backlink if the local bug isn't a
69
            # duplicate and the bug watch is associated with a bug task.
70
            # This helps us to avoid spamming both upstream and
71
            # ourselves.
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
72
            do_sync = (
10694.2.29 by Graham Binns
Fixed lint.
73
                self.bug_watch.bug.duplicateof is None and
74
                len(self.bug_watch.bugtasks) > 0
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
75
                )
76
10694.5.23 by Graham Binns
Errors in comment imports are now recorded correctly.
77
        error_message = None
78
        error_status = None
79
        oops_id = None
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
80
        if do_sync:
10694.5.23 by Graham Binns
Errors in comment imports are now recorded correctly.
81
            try:
12060.4.1 by Graham Binns
Removed unnecessary parameters for updateBugWatch().
82
                if self.can_import_comments:
10694.5.23 by Graham Binns
Errors in comment imports are now recorded correctly.
83
                    error_status = (
84
                        BugWatchActivityStatus.COMMENT_IMPORT_FAILED)
85
                    self.importBugComments()
12060.4.1 by Graham Binns
Removed unnecessary parameters for updateBugWatch().
86
                if self.can_push_comments:
10694.5.23 by Graham Binns
Errors in comment imports are now recorded correctly.
87
                    error_status = BugWatchActivityStatus.COMMENT_PUSH_FAILED
88
                    self.pushBugComments()
12060.4.1 by Graham Binns
Removed unnecessary parameters for updateBugWatch().
89
                if self.can_back_link:
10694.5.23 by Graham Binns
Errors in comment imports are now recorded correctly.
90
                    error_status = BugWatchActivityStatus.BACKLINK_FAILED
91
                    self.linkLaunchpadBug()
92
            except Exception, ex:
10637.3.9 by Guilherme Salgado
Fix lib/lp/bugs/scripts/checkwatches/bugwatchupdater.py to not use Exception.message, which deprecated in python2.6
93
                error_message = str(ex)
12599.4.2 by Leonard Richardson
Merge from trunk.
94
                log_message = (
95
                    "Failure updating bug %r on %s (local bug: %s)" %
96
                    (self.remote_bug, self.external_bugtracker.baseurl,
97
                    self.local_bug))
98
                if isinstance(ex, BugWatchUpdateError):
99
                    self.logger.info('%s: %s' % (log_message, ex))
100
                else:
101
                    oops_id = self.error(log_message, self.oops_properties)
10694.5.23 by Graham Binns
Errors in comment imports are now recorded correctly.
102
            else:
103
                error_status = None
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
104
105
        with self.transaction:
10694.5.23 by Graham Binns
Errors in comment imports are now recorded correctly.
106
            self.bug_watch.addActivity(
107
                result=error_status, message=error_message, oops_id=oops_id)
108
            self.bug_watch.last_error_type = error_status
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
109
10694.2.32 by Graham Binns
Err... Everything seems to work. That's worrying.
110
    @commit_before
10694.2.29 by Graham Binns
Fixed lint.
111
    def importBugComments(self):
10694.2.35 by Graham Binns
Fixed comment pushing and importing tests.
112
        """Import all the comments from the remote bug."""
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
113
        with self.transaction:
10694.2.29 by Graham Binns
Fixed lint.
114
            local_bug_id = self.bug_watch.bug.id
115
            remote_bug_id = self.bug_watch.remotebug
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
116
117
        # Construct a list of the comment IDs we want to import; i.e.
118
        # those which we haven't already imported.
10694.2.29 by Graham Binns
Fixed lint.
119
        all_comment_ids = self.external_bugtracker.getCommentIds(
120
            remote_bug_id)
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
121
122
        with self.transaction:
123
            comment_ids_to_import = [
124
                comment_id for comment_id in all_comment_ids
10694.2.29 by Graham Binns
Fixed lint.
125
                if not self.bug_watch.hasComment(comment_id)]
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
126
10694.2.29 by Graham Binns
Fixed lint.
127
        self.external_bugtracker.fetchComments(
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
128
            remote_bug_id, comment_ids_to_import)
129
130
        with self.transaction:
10694.2.29 by Graham Binns
Fixed lint.
131
            previous_imported_comments = (
132
                self.bug_watch.getImportedBugMessages())
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
133
            is_initial_import = previous_imported_comments.count() == 0
134
            imported_comments = []
135
136
            for comment_id in comment_ids_to_import:
10694.2.29 by Graham Binns
Fixed lint.
137
                displayname, email = (
138
                    self.external_bugtracker.getPosterForComment(
139
                        remote_bug_id, comment_id))
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
140
141
                if displayname is None and email is None:
142
                    # If we don't have a displayname or an email address
143
                    # then we can't create a Launchpad Person as the author
144
                    # of this comment. We raise an OOPS and continue.
145
                    self.warning(
146
                        "Unable to import remote comment author. No email "
147
                        "address or display name found.",
10694.2.29 by Graham Binns
Fixed lint.
148
                        get_remote_system_oops_properties(
149
                            self.external_bugtracker),
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
150
                        sys.exc_info())
151
                    continue
152
10694.2.29 by Graham Binns
Fixed lint.
153
                poster = self.bug_watch.bugtracker.ensurePersonForSelf(
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
154
                    displayname, email, PersonCreationRationale.BUGIMPORT,
10694.2.29 by Graham Binns
Fixed lint.
155
                    "when importing comments for %s." % self.bug_watch.title)
156
157
                comment_message = (
158
                    self.external_bugtracker.getMessageForComment(
159
                        remote_bug_id, comment_id, poster))
160
161
                bug_message = self.bug_watch.addComment(
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
162
                    comment_id, comment_message)
163
                imported_comments.append(bug_message)
164
165
            if len(imported_comments) > 0:
10694.2.29 by Graham Binns
Fixed lint.
166
                self.bug_watch_updater = (
10694.2.35 by Graham Binns
Fixed comment pushing and importing tests.
167
                    getUtility(ILaunchpadCelebrities).bug_watch_updater)
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
168
                if is_initial_import:
169
                    notification_text = get_email_template(
14538.2.34 by Curtis Hovey
Move email templates to lp.bugs.
170
                        'bugwatch-initial-comment-import.txt', 'bugs') % dict(
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
171
                            num_of_comments=len(imported_comments),
10694.2.29 by Graham Binns
Fixed lint.
172
                            bug_watch_url=self.bug_watch.url)
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
173
                    comment_text_template = get_email_template(
14538.2.34 by Curtis Hovey
Move email templates to lp.bugs.
174
                        'bugwatch-comment.txt', 'bugs')
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
175
176
                    for bug_message in imported_comments:
177
                        comment = bug_message.message
178
                        notification_text += comment_text_template % dict(
179
                            comment_date=comment.datecreated.isoformat(),
180
                            commenter=comment.owner.displayname,
181
                            comment_text=comment.text_contents,
182
                            comment_reply_url=canonical_url(comment))
183
                    notification_message = getUtility(IMessageSet).fromText(
10694.2.29 by Graham Binns
Fixed lint.
184
                        subject=self.bug_watch.bug.followup_subject(),
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
185
                        content=notification_text,
10694.2.29 by Graham Binns
Fixed lint.
186
                        owner=self.bug_watch_updater)
187
                    self.bug_watch.bug.addCommentNotification(
188
                        notification_message)
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
189
                else:
190
                    for bug_message in imported_comments:
191
                        notify(ObjectCreatedEvent(
192
                            bug_message,
10694.2.29 by Graham Binns
Fixed lint.
193
                            user=self.bug_watch_updater))
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
194
195
            self.logger.info("Imported %(count)i comments for remote bug "
196
                "%(remotebug)s on %(bugtracker_url)s into Launchpad bug "
197
                "%(bug_id)s." %
198
                {'count': len(imported_comments),
199
                 'remotebug': remote_bug_id,
10694.2.29 by Graham Binns
Fixed lint.
200
                 'bugtracker_url': self.external_bugtracker.baseurl,
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
201
                 'bug_id': local_bug_id})
202
10694.2.29 by Graham Binns
Fixed lint.
203
    def _formatRemoteComment(self, message):
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
204
        """Format a comment for a remote bugtracker and return it."""
205
        comment_template = get_email_template(
14538.2.34 by Curtis Hovey
Move email templates to lp.bugs.
206
            self.external_bugtracker.comment_template, 'bugs')
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
207
208
        return comment_template % {
10694.2.29 by Graham Binns
Fixed lint.
209
            'launchpad_bug': self.bug_watch.bug.id,
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
210
            'comment_author': message.owner.displayname,
211
            'comment_body': message.text_contents,
212
            }
213
10694.2.32 by Graham Binns
Err... Everything seems to work. That's worrying.
214
    @commit_before
10694.2.29 by Graham Binns
Fixed lint.
215
    def pushBugComments(self):
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
216
        """Push Launchpad comments to the remote bug.
217
10694.2.29 by Graham Binns
Fixed lint.
218
        :param self.external_bugtracker: An external bugtracker which
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
219
            implements `ISupportsCommentPushing`.
10694.2.29 by Graham Binns
Fixed lint.
220
        :param self.bug_watch: The bug watch to which the comments should be
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
221
            pushed.
222
        """
223
        pushed_comments = 0
224
225
        with self.transaction:
10694.2.29 by Graham Binns
Fixed lint.
226
            local_bug_id = self.bug_watch.bug.id
227
            remote_bug_id = self.bug_watch.remotebug
228
            unpushed_comments = list(self.bug_watch.unpushed_comments)
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
229
230
        # Loop over the unpushed comments for the bug watch.
231
        # We only push those comments that haven't been pushed
232
        # already. We don't push any comments not associated with
233
        # the bug watch.
234
        for unpushed_comment in unpushed_comments:
235
            with self.transaction:
236
                message = unpushed_comment.message
237
                message_rfc822msgid = message.rfc822msgid
238
                # Format the comment so that it includes information
239
                # about the Launchpad bug.
10694.2.35 by Graham Binns
Fixed comment pushing and importing tests.
240
                formatted_comment = self._formatRemoteComment(message)
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
241
242
            remote_comment_id = (
10694.2.29 by Graham Binns
Fixed lint.
243
                self.external_bugtracker.addRemoteComment(
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
244
                    remote_bug_id, formatted_comment,
245
                    message_rfc822msgid))
246
247
            assert remote_comment_id is not None, (
248
                "A remote_comment_id must be specified.")
249
            with self.transaction:
250
                unpushed_comment.remote_comment_id = remote_comment_id
251
252
            pushed_comments += 1
253
254
        if pushed_comments > 0:
255
            self.logger.info("Pushed %(count)i comments to remote bug "
256
                "%(remotebug)s on %(bugtracker_url)s from Launchpad bug "
257
                "%(bug_id)s" %
258
                {'count': pushed_comments,
259
                 'remotebug': remote_bug_id,
10694.2.29 by Graham Binns
Fixed lint.
260
                 'bugtracker_url': self.external_bugtracker.baseurl,
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
261
                 'bug_id': local_bug_id})
262
10694.2.32 by Graham Binns
Err... Everything seems to work. That's worrying.
263
    @commit_before
10694.2.29 by Graham Binns
Fixed lint.
264
    def linkLaunchpadBug(self):
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
265
        """Link a Launchpad bug to a given remote bug."""
266
        with self.transaction:
10694.2.29 by Graham Binns
Fixed lint.
267
            local_bug_id = self.bug_watch.bug.id
268
            local_bug_url = canonical_url(self.bug_watch.bug)
269
            remote_bug_id = self.bug_watch.remotebug
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
270
10694.2.29 by Graham Binns
Fixed lint.
271
        current_launchpad_id = self.external_bugtracker.getLaunchpadBugId(
272
            remote_bug_id)
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
273
274
        if current_launchpad_id is None:
275
            # If no bug is linked to the remote bug, link this one and
276
            # then stop.
10694.2.29 by Graham Binns
Fixed lint.
277
            self.external_bugtracker.setLaunchpadBugId(
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
278
                remote_bug_id, local_bug_id, local_bug_url)
279
            return
280
281
        elif current_launchpad_id == local_bug_id:
282
            # If the current_launchpad_id is the same as the ID of the bug
283
            # we're trying to link, we can stop.
284
            return
285
286
        else:
287
            # If the current_launchpad_id isn't the same as the one
288
            # we're trying to link, check that the other bug actually
289
            # links to the remote bug. If it does, we do nothing, since
290
            # the first valid link wins. Otherwise we link the bug that
291
            # we've been passed, overwriting the previous value of the
292
            # Launchpad bug ID for this remote bug.
293
            try:
294
                with self.transaction:
295
                    other_launchpad_bug = getUtility(IBugSet).get(
296
                        current_launchpad_id)
297
                    other_bug_watch = other_launchpad_bug.getBugWatch(
10694.2.29 by Graham Binns
Fixed lint.
298
                        self.bug_watch.bugtracker, remote_bug_id)
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
299
            except NotFoundError:
300
                # If we can't find the bug that's referenced by
10694.2.29 by Graham Binns
Fixed lint.
301
                # current_launchpad_id we simply set other_self.bug_watch to
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
302
                # None so that the Launchpad ID of the remote bug can be
303
                # set correctly.
304
                other_bug_watch = None
305
306
            if other_bug_watch is None:
10694.2.29 by Graham Binns
Fixed lint.
307
                self.external_bugtracker.setLaunchpadBugId(
10694.2.28 by Graham Binns
Moved BugWatch-specific checkwatches code into a BugWatchUpdater class.
308
                    remote_bug_id, local_bug_id, local_bug_url)