~launchpad-pqm/launchpad/devel

14538.1.3 by Curtis Hovey
Updated copyright.
1
# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
8687.15.15 by Karl Fogel
Add the copyright header block to files under lib/lp/bugs/.
2
# GNU Affero General Public License version 3 (see the file LICENSE).
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
3
3691.440.3 by James Henstridge
add the bug ID map cache save/load code
4
"""An XML bug importer
5
6
This code can import an XML bug dump into Launchpad.  The XML format
7
is described in the RELAX-NG schema 'doc/bug-export.rnc'.
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
8
"""
9
3691.440.3 by James Henstridge
add the bug ID map cache save/load code
10
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
11
__metaclass__ = type
12
13
__all__ = [
3691.440.3 by James Henstridge
add the bug ID map cache save/load code
14
    'BugXMLSyntaxError',
15
    'BugImporter',
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
16
    ]
17
3691.440.3 by James Henstridge
add the bug ID map cache save/load code
18
import cPickle
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
19
from cStringIO import StringIO
20
import datetime
21
import logging
22
import os
23
import time
24
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
25
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
26
try:
9328.2.1 by Max Bowsher
Modify all imports of cElementTree to try both the Python 2.5+ and Python 2.4 names for the module.
27
    import xml.etree.cElementTree as ET
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
28
except ImportError:
29
    import cElementTree as ET
30
3691.440.3 by James Henstridge
add the bug ID map cache save/load code
31
import pytz
32
5821.13.1 by Bjorn Tillenius
flush after a new milestone has been created by the bug import. makes all tests in test_bugimport.py pass.
33
from storm.store import Store
34
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
35
from zope.component import getUtility
6061.2.8 by Maris Fogels
Updated many more import statements
36
from zope.contenttype import guess_content_type
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
37
38
from canonical.database.constants import UTC_NOW
14538.1.2 by Curtis Hovey
Moved account and email address to lp.services.identity.
39
from lp.services.identity.interfaces.emailaddress import IEmailAddressSet
13130.1.6 by Curtis Hovey
Move ILaunchpadCelebrity to lp.app.
40
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
8523.3.1 by Gavin Panella
Bugs tree reorg after automated migration.
41
from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet
12929.9.2 by j.c.sackett
Moved messages from canonical to lp.services
42
from lp.services.messages.interfaces.message import IMessageSet
8523.3.1 by Gavin Panella
Bugs tree reorg after automated migration.
43
from lp.bugs.interfaces.bug import CreateBugParams, IBugSet
44
from lp.bugs.interfaces.bugactivity import IBugActivitySet
7675.304.1 by Gavin Panella
First bash at creating and linking Persons when Accounts already exist.
45
from lp.bugs.interfaces.bugattachment import (
46
    BugAttachmentType, IBugAttachmentSet)
8523.3.1 by Gavin Panella
Bugs tree reorg after automated migration.
47
from lp.bugs.interfaces.bugtask import BugTaskImportance, BugTaskStatus
48
from lp.bugs.interfaces.bugtracker import IBugTrackerSet
49
from lp.bugs.interfaces.bugwatch import IBugWatchSet, NoBugTrackerFound
50
from lp.bugs.interfaces.cve import ICveSet
51
from lp.registry.interfaces.person import IPersonSet, PersonCreationRationale
52
from lp.bugs.scripts.bugexport import BUGS_XMLNS
53
54
8657.3.6 by Graham Binns
BugImporter now accepts a logger in its constructor so that it can be passed the logger by the script that uses it rather than relying on getting a logger using getlogger().
55
DEFAULT_LOGGER = logging.getLogger('lp.bugs.scripts.bugimport')
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
56
57
UTC = pytz.timezone('UTC')
58
59
60
class BugXMLSyntaxError(Exception):
3691.440.14 by James Henstridge
changes and extra tests suggested in BjornT's review
61
    """A syntax error was detected in the input."""
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
62
63
64
def parse_date(datestr):
3691.440.9 by James Henstridge
add a get_text() helper, and use it where appropriate
65
    """Parse a date in the format 'YYYY-MM-DDTHH:MM:SSZ' to a dattime."""
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
66
    if datestr in ['', None]:
67
        return None
68
    year, month, day, hour, minute, second = time.strptime(
69
        datestr, '%Y-%m-%dT%H:%M:%SZ')[:6]
70
    return datetime.datetime(year, month, day, hour, minute, tzinfo=UTC)
71
72
3691.440.9 by James Henstridge
add a get_text() helper, and use it where appropriate
73
def get_text(node):
74
    """Get the text content of an element."""
75
    if node is None:
76
        return None
77
    if len(node) != 0:
78
        raise BugXMLSyntaxError('No child nodes are expected for <%s>'
79
                                % node.tag)
80
    if node.text is None:
81
        return ''
82
    return node.text.strip()
83
84
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
85
def get_enum_value(enumtype, name):
3691.440.9 by James Henstridge
add a get_text() helper, and use it where appropriate
86
    """Get the dbschema enum value with the given name."""
3691.440.14 by James Henstridge
changes and extra tests suggested in BjornT's review
87
    try:
3864.2.14 by Tim Penhey
Fixing broken tests or ensuring use of quote or sqlvalues.
88
        return enumtype.items[name]
3691.440.14 by James Henstridge
changes and extra tests suggested in BjornT's review
89
    except KeyError:
90
        raise BugXMLSyntaxError('%s is not a valid %s enumeration value' %
91
                                (name, enumtype.__name__))
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
92
93
94
def get_element(node, name):
95
    """Get the first element with the given name in the bugs XML namespace."""
96
    # alter the name to use the Launchpad bugs XML namespace
97
    name = '/'.join(['{%s}%s' % (BUGS_XMLNS, part)
98
                     for part in name.split('/')])
99
    return node.find(name)
100
101
102
def get_value(node, name):
103
    """Return the text value of the element with the given name."""
3691.440.9 by James Henstridge
add a get_text() helper, and use it where appropriate
104
    childnode = get_element(node, name)
105
    return get_text(childnode)
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
106
107
108
def get_all(node, name):
109
    """Get a list of all elements with the given name."""
110
    # alter the name to use the Launchpad bugs XML namespace
111
    name = '/'.join(['{%s}%s' % (BUGS_XMLNS, part)
112
                     for part in name.split('/')])
113
    return node.findall(name)
114
115
116
class BugImporter:
117
    """Import bugs into Launchpad"""
118
3691.440.3 by James Henstridge
add the bug ID map cache save/load code
119
    def __init__(self, product, bugs_filename, cache_filename,
12043.8.3 by Peter Clifton
bug-import: Remove the --allow-empty-comments option, check attachments instead
120
                 verify_users=False, logger=None):
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
121
        self.product = product
3691.440.3 by James Henstridge
add the bug ID map cache save/load code
122
        self.bugs_filename = bugs_filename
123
        self.cache_filename = cache_filename
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
124
        self.verify_users = verify_users
125
        self.person_id_cache = {}
126
        self.bug_importer = getUtility(ILaunchpadCelebrities).bug_importer
3691.440.3 by James Henstridge
add the bug ID map cache save/load code
127
8657.3.6 by Graham Binns
BugImporter now accepts a logger in its constructor so that it can be passed the logger by the script that uses it rather than relying on getting a logger using getlogger().
128
        if logger is None:
129
            self.logger = DEFAULT_LOGGER
130
        else:
131
            self.logger = logger
132
3691.440.3 by James Henstridge
add the bug ID map cache save/load code
133
        # A mapping of old bug IDs to new Launchpad Bug IDs
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
134
        self.bug_id_map = {}
3691.440.3 by James Henstridge
add the bug ID map cache save/load code
135
        # A mapping of old bug IDs to a list of Launchpad Bug IDs that are
136
        # duplicates of this bug.
137
        self.pending_duplicates = {}
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
138
139
    def getPerson(self, node):
140
        """Get the Launchpad user corresponding to the given XML node"""
141
        if node is None:
142
            return None
4785.3.7 by Jeroen Vermeulen
Removed whitespace at ends of lines
143
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
144
        # special case for "nobody"
145
        name = node.get('name')
146
        if name == 'nobody':
147
            return None
148
149
        # We require an email address:
150
        email = node.get('email')
151
        if email is None:
4813.13.5 by Gavin Panella
Fix pylint/pyflakes issues.
152
            raise BugXMLSyntaxError(
153
                'element %s (name=%s) has no email address'
154
                % (node.tag, name))
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
155
3691.440.9 by James Henstridge
add a get_text() helper, and use it where appropriate
156
        displayname = get_text(node)
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
157
        if not displayname:
158
            displayname = None
4785.3.7 by Jeroen Vermeulen
Removed whitespace at ends of lines
159
7675.304.8 by Gavin Panella
Use a shared IPersonSet and clarify what the name check was doing.
160
        person_set = getUtility(IPersonSet)
161
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
162
        launchpad_id = self.person_id_cache.get(email)
163
        if launchpad_id is not None:
7675.304.8 by Gavin Panella
Use a shared IPersonSet and clarify what the name check was doing.
164
            person = person_set.get(launchpad_id)
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
165
            if person is not None and person.merged is not None:
166
                person = None
167
        else:
168
            person = None
169
170
        if person is None:
7675.304.4 by Gavin Panella
Use the (very recently updated) IAccount.createPerson() call instead of trying to DIY.
171
            address = getUtility(IEmailAddressSet).getByEmail(email)
172
            if address is None:
8657.3.6 by Graham Binns
BugImporter now accepts a logger in its constructor so that it can be passed the logger by the script that uses it rather than relying on getting a logger using getlogger().
173
                self.logger.debug('creating person for %s' % email)
7675.304.4 by Gavin Panella
Use the (very recently updated) IAccount.createPerson() call instead of trying to DIY.
174
                # Has the short name been taken?
7675.304.8 by Gavin Panella
Use a shared IPersonSet and clarify what the name check was doing.
175
                if name is not None and (
176
                    person_set.getByName(name) is not None):
177
                    # The short name is already taken, so we'll pass
178
                    # None to createPersonAndEmail(), which will take
179
                    # care of creating a unique one.
180
                    name = None
7675.304.4 by Gavin Panella
Use the (very recently updated) IAccount.createPerson() call instead of trying to DIY.
181
                person, address = (
7675.304.8 by Gavin Panella
Use a shared IPersonSet and clarify what the name check was doing.
182
                    person_set.createPersonAndEmail(
7675.304.4 by Gavin Panella
Use the (very recently updated) IAccount.createPerson() call instead of trying to DIY.
183
                        email=email, name=name, displayname=displayname,
184
                        rationale=PersonCreationRationale.BUGIMPORT,
185
                        comment=('when importing bugs for %s' %
186
                                 self.product.displayname)))
187
            elif address.personID is None:
188
                # The user has an Account and and EmailAddress linked
189
                # to that account.
190
                assert address.accountID is not None, (
191
                    "Email address not linked to an Account: %s " % email)
192
                self.logger.debug(
193
                    'creating person from account for %s' % email)
7675.304.8 by Gavin Panella
Use a shared IPersonSet and clarify what the name check was doing.
194
                if name is not None and (
195
                    person_set.getByName(name) is not None):
196
                    # The short name is already taken, so we'll pass
197
                    # None to createPerson(), which will take care of
198
                    # creating a unique one.
199
                    name = None
7675.304.4 by Gavin Panella
Use the (very recently updated) IAccount.createPerson() call instead of trying to DIY.
200
                person = address.account.createPerson(
201
                    rationale=PersonCreationRationale.BUGIMPORT,
202
                    name=name, comment=('when importing bugs for %s' %
203
                                        self.product.displayname))
204
            else:
7675.304.5 by Gavin Panella
Add comment explaining why the Person is looked-up again.
205
                # EmailAddress and Person are in different stores.
7675.304.8 by Gavin Panella
Use a shared IPersonSet and clarify what the name check was doing.
206
                person = person_set.get(address.personID)
7675.304.1 by Gavin Panella
First bash at creating and linking Persons when Accounts already exist.
207
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
208
            self.person_id_cache[email] = person.id
209
210
        # if we are auto-verifying new accounts, make sure the person
211
        # has a preferred email
212
        if self.verify_users and person.preferredemail is None:
213
            address = getUtility(IEmailAddressSet).getByEmail(email)
214
            assert address is not None
215
            person.setPreferredEmail(address)
216
217
        return person
218
219
    def getMilestone(self, name):
220
        if name in ['', None]:
221
            return None
222
223
        milestone = self.product.getMilestone(name)
224
        if milestone is not None:
225
            return milestone
226
227
        # Add the milestones to the development focus series of the product
228
        series = self.product.development_focus
5821.13.1 by Bjorn Tillenius
flush after a new milestone has been created by the bug import. makes all tests in test_bugimport.py pass.
229
        milestone = series.newMilestone(name)
230
        Store.of(milestone).flush()
231
        return milestone
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
232
3691.440.3 by James Henstridge
add the bug ID map cache save/load code
233
    def loadCache(self):
234
        """Load the Bug ID mapping and pending duplicates list from cache."""
235
        if not os.path.exists(self.cache_filename):
236
            self.bug_id_map = {}
237
            self.pending_duplicates = {}
238
        else:
239
            self.bug_id_map, self.pending_duplicates = cPickle.load(
240
                open(self.cache_filename, 'rb'))
241
242
    def saveCache(self):
243
        """Save the bug ID mapping and pending duplicates list to cache."""
244
        tmpfilename = '%s.tmp' % self.cache_filename
245
        fp = open(tmpfilename, 'wb')
246
        cPickle.dump((self.bug_id_map, self.pending_duplicates),
247
                     fp, protocol=2)
248
        fp.close()
249
        os.rename(tmpfilename, self.cache_filename)
250
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
251
    def haveImportedBug(self, bugnode):
252
        """Return True if the given bug has been imported already."""
253
        bug_id = int(bugnode.get('id'))
4664.1.1 by Curtis Hovey
Normalized comments for bug 3732.
254
        # XXX: jamesh 2007-03-16:
3691.440.14 by James Henstridge
changes and extra tests suggested in BjornT's review
255
        # This should be extended to cover other cases like identity
256
        # based on bug nickname.
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
257
        return bug_id in self.bug_id_map
258
259
    def importBugs(self, ztm):
260
        """Import bugs from a file."""
3691.440.3 by James Henstridge
add the bug ID map cache save/load code
261
        tree = ET.parse(self.bugs_filename)
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
262
        root = tree.getroot()
3691.440.14 by James Henstridge
changes and extra tests suggested in BjornT's review
263
        assert root.tag == '{%s}launchpad-bugs' % BUGS_XMLNS, (
264
            "Root element is wrong: %s" % root.tag)
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
265
        for bugnode in get_all(root, 'bug'):
266
            if self.haveImportedBug(bugnode):
267
                continue
268
            ztm.begin()
269
            try:
3691.440.3 by James Henstridge
add the bug ID map cache save/load code
270
                # The cache is loaded before we import the bug so that
271
                # changes to the bug mapping and pending duplicates
272
                # made by failed bug imports don't affect this bug.
273
                self.loadCache()
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
274
                self.importBug(bugnode)
3691.440.3 by James Henstridge
add the bug ID map cache save/load code
275
                self.saveCache()
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
276
            except (SystemExit, KeyboardInterrupt):
277
                raise
278
            except:
8657.3.7 by Graham Binns
Review changes for allenap.
279
                self.logger.exception(
280
                    'Could not import bug #%s', bugnode.get('id'))
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
281
                ztm.abort()
282
            else:
283
                ztm.commit()
284
285
    def importBug(self, bugnode):
3691.440.14 by James Henstridge
changes and extra tests suggested in BjornT's review
286
        assert not self.haveImportedBug(bugnode), (
287
            'the bug has already been imported')
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
288
        bug_id = int(bugnode.get('id'))
8657.3.6 by Graham Binns
BugImporter now accepts a logger in its constructor so that it can be passed the logger by the script that uses it rather than relying on getting a logger using getlogger().
289
290
        self.logger.info('Handling bug %d', bug_id)
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
291
292
        comments = get_all(bugnode, 'comment')
293
294
        owner = self.getPerson(get_element(bugnode, 'reporter'))
295
        datecreated = parse_date(get_value(bugnode, 'datecreated'))
296
        title = get_value(bugnode, 'title')
3691.440.14 by James Henstridge
changes and extra tests suggested in BjornT's review
297
298
        private = get_value(bugnode, 'private') == 'True'
299
        security_related = get_value(bugnode, 'security_related') == 'True'
300
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
301
        if owner is None:
302
            owner = self.bug_importer
303
        commentnode = comments.pop(0)
304
        msg = self.createMessage(commentnode, defaulttitle=title)
305
306
        bug = self.product.createBug(CreateBugParams(
307
            msg=msg,
308
            datecreated=datecreated,
309
            title=title,
4813.12.28 by Gavin Panella
Undo a small change to bug importing which caused other problems, and add a comment so others don't make the same mistake.
310
            private=private or security_related,
3691.440.14 by James Henstridge
changes and extra tests suggested in BjornT's review
311
            security_related=security_related,
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
312
            owner=owner))
4813.12.28 by Gavin Panella
Undo a small change to bug importing which caused other problems, and add a comment so others don't make the same mistake.
313
        # Security related bugs must be created private, so we set it
314
        # correctly after creation.
315
        bug.setPrivate(private, owner)
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
316
        bugtask = bug.bugtasks[0]
8657.3.6 by Graham Binns
BugImporter now accepts a logger in its constructor so that it can be passed the logger by the script that uses it rather than relying on getting a logger using getlogger().
317
        self.logger.info('Creating Launchpad bug #%d', bug.id)
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
318
319
        # Remaining setup for first comment
320
        self.createAttachments(bug, msg, commentnode)
4476.1.3 by Bjorn Tillenius
fix test to expose problem when creating CVEs on package uploads. Fix the test failure by requiring a user attribute for linkCVE and findCvesInText.
321
        bug.findCvesInText(msg.text_contents, bug.owner)
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
322
323
        # Process remaining comments
324
        for commentnode in comments:
325
            msg = self.createMessage(commentnode,
326
                                     defaulttitle=bug.followup_subject())
327
            bug.linkMessage(msg)
328
            self.createAttachments(bug, msg, commentnode)
329
330
        # set up bug
13994.2.1 by Ian Booth
Implement new subscription behaviour
331
        private = get_value(bugnode, 'private') == 'True'
332
        security_related = get_value(bugnode, 'security_related') == 'True'
333
        bug.setPrivacyAndSecurityRelated(private, security_related, owner)
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
334
        bug.name = get_value(bugnode, 'nickname')
335
        description = get_value(bugnode, 'description')
336
        if description:
337
            bug.description = description
338
339
        for cvenode in get_all(bugnode, 'cves/cve'):
3691.440.9 by James Henstridge
add a get_text() helper, and use it where appropriate
340
            cve = getUtility(ICveSet)[get_text(cvenode)]
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
341
            if cve is None:
342
                raise BugXMLSyntaxError('Unknown CVE: %s' %
3691.440.9 by James Henstridge
add a get_text() helper, and use it where appropriate
343
                                        get_text(cvenode))
4476.1.3 by Bjorn Tillenius
fix test to expose problem when creating CVEs on package uploads. Fix the test failure by requiring a user attribute for linkCVE and findCvesInText.
344
            bug.linkCVE(cve, self.bug_importer)
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
345
346
        tags = []
347
        for tagnode in get_all(bugnode, 'tags/tag'):
3691.440.9 by James Henstridge
add a get_text() helper, and use it where appropriate
348
            tags.append(get_text(tagnode))
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
349
        bug.tags = tags
350
4864.1.1 by James Henstridge
Add support for creating bug watches as part of a bug import.
351
        # Create bugwatches
352
        bugwatchset = getUtility(IBugWatchSet)
353
        for watchnode in get_all(bugnode, 'bugwatches/bugwatch'):
354
            try:
355
                bugtracker, remotebug = bugwatchset.extractBugTrackerAndBug(
356
                    watchnode.get('href'))
357
            except NoBugTrackerFound, exc:
8657.3.7 by Graham Binns
Review changes for allenap.
358
                self.logger.debug(
359
                    'Registering bug tracker for %s', exc.base_url)
4864.1.1 by James Henstridge
Add support for creating bug watches as part of a bug import.
360
                bugtracker = getUtility(IBugTrackerSet).ensureBugTracker(
361
                    exc.base_url, self.bug_importer, exc.bugtracker_type)
362
                remotebug = exc.remote_bug
363
            bugwatchset.createBugWatch(
364
                bug, self.bug_importer, bugtracker, remotebug)
365
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
366
        for subscribernode in get_all(bugnode, 'subscriptions/subscriber'):
367
            person = self.getPerson(subscribernode)
3691.440.20 by James Henstridge
handle the case of a "nobody" bug subscriber
368
            if person is not None:
5454.1.5 by Tom Berger
record who created each bug subscription, and display the result in the title of the subscriber link
369
                bug.subscribe(person, owner)
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
370
371
        # set up bug task
372
        bugtask.datecreated = datecreated
6602.5.3 by Tom Berger
convert all assingments to bugtask.importance to calls to transitionToImportance
373
        bugtask.transitionToImportance(
374
            get_enum_value(BugTaskImportance,
375
                           get_value(bugnode, 'importance')),
376
            self.bug_importer)
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
377
        bugtask.transitionToStatus(
4318.3.10 by Gavin Panella
Changing transitionToStatus to accept user argument.
378
            get_enum_value(BugTaskStatus, get_value(bugnode, 'status')),
379
            self.bug_importer)
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
380
        bugtask.transitionToAssignee(
381
            self.getPerson(get_element(bugnode, 'assignee')))
382
        bugtask.milestone = self.getMilestone(get_value(bugnode, 'milestone'))
383
384
        # Make a note of the import in the activity log:
385
        getUtility(IBugActivitySet).new(
386
            bug=bug.id,
387
            datechanged=UTC_NOW,
388
            person=self.bug_importer,
389
            whatchanged='bug',
390
            message='Imported external bug #%s' % bug_id)
391
3691.440.3 by James Henstridge
add the bug ID map cache save/load code
392
        self.handleDuplicate(bug, bug_id, get_value(bugnode, 'duplicateof'))
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
393
        self.bug_id_map[bug_id] = bug.id
3691.440.23 by James Henstridge
expire pending bug notifications for newly created bugs
394
395
        # clear any pending bug notifications
396
        bug.expireNotifications()
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
397
        return bug
398
399
    def createMessage(self, commentnode, defaulttitle=None):
400
        """Create an IMessage representing a <comment> element."""
401
        title = get_value(commentnode, 'title')
402
        if title is None:
403
            title = defaulttitle
404
        sender = self.getPerson(get_element(commentnode, 'sender'))
405
        if sender is None:
406
            sender = self.bug_importer
407
        date = parse_date(get_value(commentnode, 'date'))
408
        if date is None:
409
            raise BugXMLSyntaxError('No date for comment %r' % title)
410
        text = get_value(commentnode, 'text')
12043.8.5 by Peter Clifton
lib/lp/bugs/scripts/bugimport.py: Re-arrange test to increase code clarity
411
        # If there is no comment text and no attachment, use a place-holder
412
        if ((text is None or text == '') and
413
            get_element(commentnode, 'attachment') is None):
12043.8.3 by Peter Clifton
bug-import: Remove the --allow-empty-comments option, check attachments instead
414
            text = '<empty comment>'
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
415
        return getUtility(IMessageSet).fromText(title, text, sender, date)
416
417
    def createAttachments(self, bug, message, commentnode):
418
        """Create attachments that were attached to the given comment."""
419
        for attachnode in get_all(commentnode, 'attachment'):
420
            if get_value(attachnode, 'type'):
421
                attach_type = get_enum_value(BugAttachmentType,
422
                                             get_value(attachnode, 'type'))
423
            else:
424
                attach_type = BugAttachmentType.UNSPECIFIED
425
            filename = get_value(attachnode, 'filename')
426
            title = get_value(attachnode, 'title')
427
            mimetype = get_value(attachnode, 'mimetype')
428
            contents = get_value(attachnode, 'contents').decode('base-64')
429
            if filename is None:
3691.440.11 by James Henstridge
take filename from href if not given explicitly
430
                # if filename is None, use the last component of the URL
431
                if attachnode.get('href') is not None:
432
                    filename = attachnode.get('href').split('/')[-1]
433
                else:
434
                    filename = 'unknown'
3691.440.1 by James Henstridge
rebase bug-import stuff on latest rocketfuel
435
            if title is None:
436
                title = filename
437
            # force mimetype to text/plain if it is a patch
438
            if attach_type == BugAttachmentType.PATCH:
439
                mimetype = 'text/plain'
440
            # If we don't have a mime type, or it is classed as
441
            # straight binary data, sniff the mimetype
442
            if (mimetype is None or
443
                mimetype.startswith('application/octet-stream')):
444
                mimetype, encoding = guess_content_type(
445
                    name=filename, body=contents)
446
447
            # Create the file in the librarian
448
            filealias = getUtility(ILibraryFileAliasSet).create(
449
                name=filename,
450
                size=len(contents),
451
                file=StringIO(contents),
452
                contentType=mimetype)
453
454
            getUtility(IBugAttachmentSet).create(
455
                bug=bug,
456
                filealias=filealias,
457
                attach_type=attach_type,
458
                title=title,
459
                message=message)
3691.440.3 by James Henstridge
add the bug ID map cache save/load code
460
461
    def handleDuplicate(self, bug, bug_id, duplicateof=None):
462
        """Handle duplicate processing for the given bug report."""
463
        # update the bug ID map
464
        self.bug_id_map[bug_id] = bug.id
465
        # Are there any pending bugs that are duplicates of this bug?
466
        if bug_id in self.pending_duplicates:
467
            for other_bug_id in self.pending_duplicates[bug_id]:
468
                other_bug = getUtility(IBugSet).get(other_bug_id)
8657.3.7 by Graham Binns
Review changes for allenap.
469
                self.logger.info(
470
                    'Marking bug %d as duplicate of bug %d',
471
                    other_bug.id, bug.id)
11272.1.1 by Deryck Hodge
First pass at getting my dupe finder work that was
472
                other_bug.markAsDuplicate(bug)
3691.440.3 by James Henstridge
add the bug ID map cache save/load code
473
            del self.pending_duplicates[bug_id]
474
        # Process this bug as a duplicate
475
        if duplicateof is not None:
476
            duplicateof = int(duplicateof)
477
            # Have we already imported the bug?
478
            if duplicateof in self.bug_id_map:
479
                other_bug = getUtility(IBugSet).get(
480
                    self.bug_id_map[duplicateof])
8657.3.7 by Graham Binns
Review changes for allenap.
481
                self.logger.info(
482
                    'Marking bug %d as duplicate of bug %d',
483
                    bug.id, other_bug.id)
11272.1.1 by Deryck Hodge
First pass at getting my dupe finder work that was
484
                bug.markAsDuplicate(other_bug)
3691.440.3 by James Henstridge
add the bug ID map cache save/load code
485
            else:
486
                self.pending_duplicates.setdefault(
487
                    duplicateof, []).append(bug.id)