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) |