Merge lp:~gmb/launchpad/bugzilla-3.4-sync-statuses-bug-415774 into lp:launchpad
- bugzilla-3.4-sync-statuses-bug-415774
- Merge into devel
Status: | Merged |
---|---|
Merged at revision: | not available |
Proposed branch: | lp:~gmb/launchpad/bugzilla-3.4-sync-statuses-bug-415774 |
Merge into: | lp:launchpad |
Diff against target: | None lines |
To merge this branch: | bzr merge lp:~gmb/launchpad/bugzilla-3.4-sync-statuses-bug-415774 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Abel Deuring (community) | code | Approve | |
Review via email: mp+10520@code.launchpad.net |
Commit message
Description of the change
Graham Binns (gmb) wrote : | # |
Abel Deuring (adeuring) wrote : | # |
Hi Graham,
again, no complaints except a trailing space ;)
r=me
Abel
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -301,34 +301,10 @@
> Getting remote statuses
> -------
>
> -BugzillaLPPlug
> -given bug as a string. If the bug has a resolution, that will be
> -returned too.
> -
> - >>> print bugzilla.
> - RESOLVED FIXED
> -
> - >>> print bugzilla.
> - NEW
> -
> -If a bug can't be found a BugNotFound error will be raised.
> -
> - >>> bugzilla.
> - Traceback (most recent call last):
> - ...
> - BugNotFound: no-such-bug
> -
> -If the data we've imported from Bugzilla is incomplete and doesn't
> -contain either the bug's status or its resolution an UnparseableBugData
> -error will be raised. We can add a sample bug to demonstrate this.
> -
> - >>> bugzilla._bugs[999] = {}
> - >>> bugzilla.
> - Traceback (most recent call last):
> - ...
> - UnparseableBugData
> -
> - >>> del bugzilla._bugs[999]
> +BugzillaLPPlugin doesn't have any special functionality for getting
> +remote statuses. See the "Getting remote statuses" section of
The line above has the trailing space.
> +externalbugtra
> +statuses from Bugzilla APIs.
Preview Diff
1 | === modified file 'lib/lp/bugs/doc/externalbugtracker-bugzilla-api.txt' |
2 | --- lib/lp/bugs/doc/externalbugtracker-bugzilla-api.txt 2009-08-19 13:12:30 +0000 |
3 | +++ lib/lp/bugs/doc/externalbugtracker-bugzilla-api.txt 2009-08-21 11:00:38 +0000 |
4 | @@ -75,3 +75,182 @@ |
5 | ... |
6 | BugTrackerAuthenticationError: http://thiswillfail.example.com: |
7 | Fault 300: The username or password you entered is not valid. |
8 | + |
9 | + |
10 | +Getting the server time |
11 | +----------------------- |
12 | + |
13 | +To be able to accurately sync with a bug tracker, we need to be able to |
14 | +check the time on the remote server. We use BugzillaAPI.getCurrentDBTime() |
15 | +to get the current time on the remote server. |
16 | + |
17 | + # There's no way to create a UTC timestamp without monkey-patching |
18 | + # the TZ environment variable. Rather than do that, we create our |
19 | + # own datetime and work with that. |
20 | + >>> from datetime import datetime |
21 | + >>> remote_time = datetime(2009, 8, 19, 17, 2, 2) |
22 | + |
23 | + >>> test_transport.local_datetime = remote_time |
24 | + >>> bugzilla.getCurrentDBTime() |
25 | + CALLED Bugzilla.time() |
26 | + datetime.datetime(2009, 8, 19, 17, 2, 2, tzinfo=<UTC>) |
27 | + |
28 | +If the remote system is in a different timezone, getCurrentDBTime() will |
29 | +convert its time to UTC before returning it. |
30 | + |
31 | + >>> test_transport.utc_offset = 60**2 |
32 | + >>> test_transport.timezone = 'CET' |
33 | + >>> bugzilla.getCurrentDBTime() |
34 | + CALLED Bugzilla.time() |
35 | + datetime.datetime(2009, 8, 19, 16, 2, 2, tzinfo=<UTC>) |
36 | + |
37 | +This works whether the UTC offset is positive or negative. |
38 | + |
39 | + >>> test_transport.utc_offset = -5 * 60**2 |
40 | + >>> test_transport.timezone = 'US/Eastern' |
41 | + >>> bugzilla.getCurrentDBTime() |
42 | + CALLED Bugzilla.time() |
43 | + datetime.datetime(2009, 8, 19, 22, 2, 2, tzinfo=<UTC>) |
44 | + |
45 | + |
46 | +Initializing the bug database |
47 | +----------------------------- |
48 | + |
49 | +BugzillaAPI implements IExternalBugTracker.initializeRemoteBugDB(), |
50 | +which takes a list of bug IDs to fetch from the remote server and stores |
51 | +those bugs locally for future use. |
52 | + |
53 | + >>> bugzilla.initializeRemoteBugDB([1, 2]) |
54 | + CALLED Bug.get({'ids': [1, 2], 'permissive': True}) |
55 | + |
56 | +The bug data is stored as a list of dicts: |
57 | + |
58 | + >>> def print_bugs(bugs): |
59 | + ... for bug in sorted(bugs): |
60 | + ... print "Bug %s:" % bug |
61 | + ... for key in sorted(bugzilla._bugs[bug]): |
62 | + ... print " %s: %s" % (key, bugzilla._bugs[bug][key]) |
63 | + ... print "\n" |
64 | + |
65 | + >>> print_bugs(bugzilla._bugs) |
66 | + Bug 1: |
67 | + alias: |
68 | + assigned_to: test@canonical.com |
69 | + component: GPPSystems |
70 | + creation_time: 20080610T16:19:53 |
71 | + id: 1 |
72 | + internals:... |
73 | + is_open: True |
74 | + last_change_time: 20080610T16:19:53 |
75 | + priority: P1 |
76 | + product: Marvin |
77 | + resolution: FIXED |
78 | + severity: normal |
79 | + status: RESOLVED |
80 | + summary: That bloody robot still exists. |
81 | + <BLANKLINE> |
82 | + Bug 2: |
83 | + alias: bug-two |
84 | + assigned_to: marvin@heartofgold.ship |
85 | + component: Crew |
86 | + creation_time: 20080611T09:23:12 |
87 | + id: 2 |
88 | + internals:... |
89 | + is_open: True |
90 | + last_change_time: 20080611T09:24:29 |
91 | + priority: P1 |
92 | + product: HeartOfGold |
93 | + resolution: |
94 | + severity: high |
95 | + status: NEW |
96 | + summary: Collect unknown persons in docking bay 2. |
97 | + <BLANKLINE> |
98 | + <BLANKLINE> |
99 | + |
100 | + |
101 | +Storing bugs |
102 | +------------ |
103 | + |
104 | +initializeRemoteBugDB() uses the _storeBugs() method to store bug data. |
105 | +_storeBugs() will only store a bug once, even if it is requested both by |
106 | +alias and ID. We'll reset the test BugzillaAPI's _bugs and _bug_aliases |
107 | +dicts to demonstrate this. |
108 | + |
109 | + >>> bugzilla._bugs = {} |
110 | + >>> bugzilla._bug_aliases = {} |
111 | + >>> bugzilla.initializeRemoteBugDB([2, 'bug-two']) |
112 | + CALLED Bug.get({'ids': [2, 'bug-two'], 'permissive': True}) |
113 | + |
114 | + >>> for bug in sorted(bugzilla._bugs): |
115 | + ... print "Bug %r:" % bug |
116 | + ... for key in sorted(bugzilla._bugs[bug]): |
117 | + ... print " %s: %s" % (key, bugzilla._bugs[bug][key]) |
118 | + ... print "\n" |
119 | + Bug 2: |
120 | + alias: bug-two |
121 | + assigned_to: marvin@heartofgold.ship |
122 | + component: Crew |
123 | + creation_time: 20080611T09:23:12 |
124 | + id: 2 |
125 | + internals:... |
126 | + is_open: True |
127 | + last_change_time: 20080611T09:24:29 |
128 | + priority: P1 |
129 | + product: HeartOfGold |
130 | + resolution: |
131 | + severity: high |
132 | + status: NEW |
133 | + summary: Collect unknown persons in docking bay 2. |
134 | + <BLANKLINE> |
135 | + <BLANKLINE> |
136 | + |
137 | +Aliases are stored in a separate dict, which contains a mapping between |
138 | +the alias and the bug's actual ID. |
139 | + |
140 | + >>> for alias, bug_id in bugzilla._bug_aliases.items(): |
141 | + ... print "%s: %s" % (alias, bug_id) |
142 | + bug-two: 2 |
143 | + |
144 | +The method _getActualBugId() returns the correct bug ID for a passed bug |
145 | +ID or alias. |
146 | + |
147 | + >>> bugzilla._getActualBugId('bug-two') |
148 | + 2 |
149 | + |
150 | + >>> bugzilla._getActualBugId(2) |
151 | + 2 |
152 | + |
153 | + |
154 | +Getting remote statuses |
155 | +----------------------- |
156 | + |
157 | +BugzillaAPI.getRemoteStatus() will return the remote status of a given |
158 | +bug as a string. If the bug has a resolution, that will be returned too. |
159 | + |
160 | + >>> test_transport.print_method_calls = False |
161 | + >>> bugzilla.initializeRemoteBugDB([1, 2]) |
162 | + |
163 | + >>> print bugzilla.getRemoteStatus(1) |
164 | + RESOLVED FIXED |
165 | + |
166 | + >>> print bugzilla.getRemoteStatus(2) |
167 | + NEW |
168 | + |
169 | +If a bug can't be found a BugNotFound error will be raised. |
170 | + |
171 | + >>> bugzilla.getRemoteStatus('no-such-bug') |
172 | + Traceback (most recent call last): |
173 | + ... |
174 | + BugNotFound: no-such-bug |
175 | + |
176 | +If the data we've imported from Bugzilla is incomplete and doesn't |
177 | +contain either the bug's status or its resolution an UnparseableBugData |
178 | +error will be raised. We can add a sample bug to demonstrate this. |
179 | + |
180 | + >>> bugzilla._bugs[999] = {} |
181 | + >>> bugzilla.getRemoteStatus(999) |
182 | + Traceback (most recent call last): |
183 | + ... |
184 | + UnparseableBugData |
185 | + |
186 | + >>> del bugzilla._bugs[999] |
187 | |
188 | === modified file 'lib/lp/bugs/doc/externalbugtracker-bugzilla-lp-plugin.txt' |
189 | --- lib/lp/bugs/doc/externalbugtracker-bugzilla-lp-plugin.txt 2009-08-19 09:22:08 +0000 |
190 | +++ lib/lp/bugs/doc/externalbugtracker-bugzilla-lp-plugin.txt 2009-08-21 11:00:38 +0000 |
191 | @@ -1,4 +1,5 @@ |
192 | -= Bugzilla bugtrackers with the Launchpad plugin = |
193 | +Bugzilla bugtrackers with the Launchpad plugin |
194 | +============================================== |
195 | |
196 | These tests cover the BugzillaLPPlugin ExternalBugTracker, which handles |
197 | Bugzilla instances that have the Launchpad plugin installed. |
198 | @@ -25,7 +26,8 @@ |
199 | True |
200 | |
201 | |
202 | -== Authentication == |
203 | +Authentication |
204 | +-------------- |
205 | |
206 | XML-RPC methods that modify data on the remote server require |
207 | authentication. To authenticate, we create a LoginToken of type |
208 | @@ -154,7 +156,8 @@ |
209 | Protocol error: 500 "Internal server error" |
210 | |
211 | |
212 | -== Getting the current time == |
213 | +Getting the current time |
214 | +------------------------ |
215 | |
216 | The BugzillaLPPlugin ExternalBugTracker, like all other |
217 | ExternalBugTrackers, has a getCurrentDBTime() method, which returns the |
218 | @@ -173,7 +176,8 @@ |
219 | datetime.datetime(2008, 5, 16, 15, 53, 20, tzinfo=<UTC>) |
220 | |
221 | |
222 | -== Initializing the remote bug database == |
223 | +Initializing the remote bug database |
224 | +------------------------------------ |
225 | |
226 | The BugzillaLPPlugin implements the standard initializeRemoteBugDB() |
227 | method, taking a list of the bug ids that need to be updated. It uses |
228 | @@ -228,56 +232,13 @@ |
229 | <BLANKLINE> |
230 | <BLANKLINE> |
231 | |
232 | -initializeRemoteBugDB() will only store a bug once, even if it is |
233 | -requested both by alias and ID. We'll reset the test BugzillaLPPlugin's |
234 | -_bugs and _bug_aliases dicts to demonstrate this. |
235 | - |
236 | - >>> bugzilla._bugs = {} |
237 | - >>> bugzilla._bug_aliases = {} |
238 | - >>> bugzilla.initializeRemoteBugDB([2, 'bug-two']) |
239 | - CALLED Launchpad.get_bugs({'ids': [2, 'bug-two'], 'permissive': True}) |
240 | - |
241 | - >>> for bug in sorted(bugzilla._bugs): |
242 | - ... print "Bug %r:" % bug |
243 | - ... for key in sorted(bugzilla._bugs[bug]): |
244 | - ... print " %s: %s" % (key, bugzilla._bugs[bug][key]) |
245 | - ... print "\n" |
246 | - Bug 2: |
247 | - alias: bug-two |
248 | - assigned_to: marvin@heartofgold.ship |
249 | - component: Crew |
250 | - creation_time: 20080611T09:23:12 |
251 | - id: 2 |
252 | - internals:... |
253 | - is_open: True |
254 | - last_change_time: 20080611T09:24:29 |
255 | - priority: P1 |
256 | - product: HeartOfGold |
257 | - resolution: |
258 | - severity: high |
259 | - status: NEW |
260 | - summary: Collect unknown persons in docking bay 2. |
261 | - <BLANKLINE> |
262 | - <BLANKLINE> |
263 | - |
264 | -Aliases are stored in a separate dict, which contains a mapping between |
265 | -the alias and the bug's actual ID. |
266 | - |
267 | - >>> for alias, bug_id in bugzilla._bug_aliases.items(): |
268 | - ... print "%s: %s" % (alias, bug_id) |
269 | - bug-two: 2 |
270 | - |
271 | -The method _getActualBugId() returns the correct bug ID for a passed bug |
272 | -ID or alias. |
273 | - |
274 | - >>> bugzilla._getActualBugId('bug-two') |
275 | - 2 |
276 | - |
277 | - >>> bugzilla._getActualBugId(2) |
278 | - 2 |
279 | - |
280 | - |
281 | -== Getting a list of changed bugs == |
282 | +BugzillaLPPlugin.initializeRemoteBugDB() uses its _storeBugs() method to |
283 | +store bugs. See externalbugtracker-bugzilla-api.txt for details of |
284 | +_storeBugs(). |
285 | + |
286 | + |
287 | +Getting a list of changed bugs |
288 | +------------------------------ |
289 | |
290 | IExternalBugTracker defines a method, getModifiedRemoteBugs(), which |
291 | accepts a list of bug IDs and a datetime as a parameter and returns the |
292 | @@ -337,39 +298,17 @@ |
293 | CALLED Launchpad.get_bugs({'ids': [3], 'permissive': True}) |
294 | |
295 | |
296 | -== Getting remote statuses == |
297 | - |
298 | -BugzillaLPPlugin.getRemoteStatus() will return the remote status of a |
299 | -given bug as a string. If the bug has a resolution, that will be |
300 | -returned too. |
301 | - |
302 | - >>> print bugzilla.getRemoteStatus(1) |
303 | - RESOLVED FIXED |
304 | - |
305 | - >>> print bugzilla.getRemoteStatus(2) |
306 | - NEW |
307 | - |
308 | -If a bug can't be found a BugNotFound error will be raised. |
309 | - |
310 | - >>> bugzilla.getRemoteStatus('no-such-bug') |
311 | - Traceback (most recent call last): |
312 | - ... |
313 | - BugNotFound: no-such-bug |
314 | - |
315 | -If the data we've imported from Bugzilla is incomplete and doesn't |
316 | -contain either the bug's status or its resolution an UnparseableBugData |
317 | -error will be raised. We can add a sample bug to demonstrate this. |
318 | - |
319 | - >>> bugzilla._bugs[999] = {} |
320 | - >>> bugzilla.getRemoteStatus(999) |
321 | - Traceback (most recent call last): |
322 | - ... |
323 | - UnparseableBugData |
324 | - |
325 | - >>> del bugzilla._bugs[999] |
326 | - |
327 | - |
328 | -== Getting the remote product == |
329 | +Getting remote statuses |
330 | +----------------------- |
331 | + |
332 | +BugzillaLPPlugin doesn't have any special functionality for getting |
333 | +remote statuses. See the "Getting remote statuses" section of |
334 | +externalbugtracker-bugzilla-api.txt for details of getting remote |
335 | +statuses from Bugzilla APIs. |
336 | + |
337 | + |
338 | +Getting the remote product |
339 | +-------------------------- |
340 | |
341 | getRemoteProduct() returns the product a remote bug is associated with |
342 | in Bugzilla. |
343 | @@ -393,7 +332,8 @@ |
344 | BugNotFound: 12345 |
345 | |
346 | |
347 | -== Retrieving remote comments == |
348 | +Retrieving remote comments |
349 | +-------------------------- |
350 | |
351 | BugzillaLPPlugin implments the ISupportsCommentImport interface, which |
352 | means that we can use it to import comments from the remote Bugzilla |
353 | @@ -435,7 +375,8 @@ |
354 | >>> LaunchpadZopelessLayer.switchDbUser(config.checkwatches.dbuser) |
355 | |
356 | |
357 | -=== getCommentIds() === |
358 | +getCommentIds() |
359 | +--------------- |
360 | |
361 | ISupportsCommentImport.getCommentIds() is the method used to get all the |
362 | comment IDs for a given bug on a remote bugtracker. |
363 | @@ -456,7 +397,8 @@ |
364 | BugNotFound: 42 |
365 | |
366 | |
367 | -=== fetchComments() === |
368 | +fetchComments() |
369 | +--------------- |
370 | |
371 | ISupportsCommentImport.fetchComments() is the method used to fetch a |
372 | given set of comments from the remote bugtracker. It takes a bug watch |
373 | @@ -488,7 +430,8 @@ |
374 | time: 20080616T13:22:29 |
375 | |
376 | |
377 | -=== getPosterForComment() === |
378 | +getPosterForComment() |
379 | +--------------------- |
380 | |
381 | ISupportsCommentImport.getPosterForComment() returns a tuple of |
382 | (displayname, email) for the author of a remote comment. |
383 | @@ -507,7 +450,8 @@ |
384 | None trillian |
385 | |
386 | |
387 | -=== getMessageForComment() === |
388 | +getMessageForComment() |
389 | +---------------------- |
390 | |
391 | ISupportsCommentImport.getMessageForComment() returns a Launchpad |
392 | IMessage instance for a given comment. It takes a bug watch, a comment |
393 | @@ -533,7 +477,8 @@ |
394 | 2008-06-16 13:08:08+00:00 |
395 | |
396 | |
397 | -== Pushing comments to remote systems == |
398 | +Pushing comments to remote systems |
399 | +---------------------------------- |
400 | |
401 | BugzillaLPPlugin implements the ISupportsCommentPushing interface, which |
402 | defines the necessary methods for pushing comments to remote servers. |
403 | @@ -583,7 +528,8 @@ |
404 | <BLANKLINE> |
405 | |
406 | |
407 | -== Linking remote bugs to Launchpad bugs == |
408 | +Linking remote bugs to Launchpad bugs |
409 | +------------------------------------- |
410 | |
411 | BugzillaLPPlugin implements the ISupportsBackLinking interface, which |
412 | provides methods to set and retrieve the Launchpad bug that links to a |
413 | @@ -644,7 +590,8 @@ |
414 | 10 |
415 | |
416 | |
417 | -== Working with a specified set of Bugzilla products == |
418 | +Working with a specified set of Bugzilla products |
419 | +------------------------------------------------- |
420 | |
421 | BugzillaLPPlugin can be instructed to only get the data for a set of |
422 | bug IDs if those bugs belong to one of a given set of products. |
423 | @@ -691,7 +638,8 @@ |
424 | 0 |
425 | |
426 | |
427 | -== Getting the products for a set of remote bugs == |
428 | +Getting the products for a set of remote bugs |
429 | +--------------------------------------------- |
430 | |
431 | BugzillaLPPlugin provides a helper method, getProductsForRemoteBugs(), |
432 | which takes a list of bug IDs or aliases and returns the products to |
433 | |
434 | === modified file 'lib/lp/bugs/externalbugtracker/bugzilla.py' |
435 | --- lib/lp/bugs/externalbugtracker/bugzilla.py 2009-08-19 13:19:22 +0000 |
436 | +++ lib/lp/bugs/externalbugtracker/bugzilla.py 2009-08-21 11:00:38 +0000 |
437 | @@ -430,6 +430,115 @@ |
438 | self.baseurl, |
439 | "Fault %s: %s" % (fault.faultCode, fault.faultString)) |
440 | |
441 | + def _storeBugs(self, remote_bugs): |
442 | + """Store remote bugs in the local `bugs` dict.""" |
443 | + for remote_bug in remote_bugs: |
444 | + self._bugs[remote_bug['id']] = remote_bug |
445 | + |
446 | + # The bug_aliases dict is a mapping between aliases and bug |
447 | + # IDs. We use the aliases dict to look up the correct ID for |
448 | + # a bug. This allows us to reference a bug by either ID or |
449 | + # alias. |
450 | + if remote_bug['alias'] != '': |
451 | + self._bug_aliases[remote_bug['alias']] = remote_bug['id'] |
452 | + |
453 | + def getCurrentDBTime(self): |
454 | + """See `IExternalBugTracker`.""" |
455 | + time_dict = self.xmlrpc_proxy.Bugzilla.time() |
456 | + |
457 | + # Convert the XML-RPC DateTime we get back into a regular Python |
458 | + # datetime. |
459 | + server_db_timetuple = time.strptime( |
460 | + str(time_dict['db_time']), '%Y%m%dT%H:%M:%S') |
461 | + server_db_datetime = datetime(*server_db_timetuple[:6]) |
462 | + |
463 | + # The server's DB time is the one that we want to use. However, |
464 | + # this may not be in UTC, so we need to convert it. Since we |
465 | + # can't guarantee that the timezone data returned by the server |
466 | + # is sane, we work out the server's offset from UTC by looking |
467 | + # at the difference between the web_time and the web_time_utc |
468 | + # values. |
469 | + server_web_time = time.strptime( |
470 | + str(time_dict['web_time']), '%Y%m%dT%H:%M:%S') |
471 | + server_web_datetime = datetime(*server_web_time[:6]) |
472 | + server_web_time_utc = time.strptime( |
473 | + str(time_dict['web_time_utc']), '%Y%m%dT%H:%M:%S') |
474 | + server_web_datetime_utc = datetime(*server_web_time_utc[:6]) |
475 | + |
476 | + server_utc_offset = server_web_datetime - server_web_datetime_utc |
477 | + server_utc_datetime = server_db_datetime - server_utc_offset |
478 | + |
479 | + return server_utc_datetime.replace(tzinfo=pytz.timezone('UTC')) |
480 | + |
481 | + def _getActualBugId(self, bug_id): |
482 | + """Return the actual bug id for an alias or id.""" |
483 | + # See if bug_id is actually an alias. |
484 | + actual_bug_id = self._bug_aliases.get(bug_id) |
485 | + |
486 | + # bug_id isn't an alias, so try turning it into an int and |
487 | + # looking the bug up by ID. |
488 | + if actual_bug_id is not None: |
489 | + return actual_bug_id |
490 | + else: |
491 | + try: |
492 | + actual_bug_id = int(bug_id) |
493 | + except ValueError: |
494 | + # If bug_id can't be int()'d then it's likely an alias |
495 | + # that doesn't exist, so raise BugNotFound. |
496 | + raise BugNotFound(bug_id) |
497 | + |
498 | + # Check that the bug does actually exist. That way we're |
499 | + # treating integer bug IDs and aliases in the same way. |
500 | + if actual_bug_id not in self._bugs: |
501 | + raise BugNotFound(bug_id) |
502 | + |
503 | + return actual_bug_id |
504 | + |
505 | + def _getBugIdsToRetrieve(self, bug_ids): |
506 | + """For a set of bug IDs, return those for which we have no data.""" |
507 | + bug_ids_to_retrieve = [] |
508 | + for bug_id in bug_ids: |
509 | + try: |
510 | + actual_bug_id = self._getActualBugId(bug_id) |
511 | + except BugNotFound: |
512 | + bug_ids_to_retrieve.append(bug_id) |
513 | + |
514 | + return bug_ids_to_retrieve |
515 | + |
516 | + def initializeRemoteBugDB(self, bug_ids): |
517 | + """See `IExternalBugTracker`.""" |
518 | + # First, discard all those bug IDs about which we already have |
519 | + # data. |
520 | + bug_ids_to_retrieve = self._getBugIdsToRetrieve(bug_ids) |
521 | + |
522 | + # Pull the bug data from the remote server. permissive=True here |
523 | + # prevents Bugzilla from erroring if we ask for a bug it doesn't |
524 | + # have. |
525 | + response_dict = self.xmlrpc_proxy.Bug.get({ |
526 | + 'ids': bug_ids_to_retrieve, |
527 | + 'permissive': True, |
528 | + }) |
529 | + remote_bugs = response_dict['bugs'] |
530 | + |
531 | + self._storeBugs(remote_bugs) |
532 | + |
533 | + def getRemoteStatus(self, bug_id): |
534 | + """See `IExternalBugTracker`.""" |
535 | + actual_bug_id = self._getActualBugId(bug_id) |
536 | + |
537 | + # Attempt to get the status and resolution from the bug. If |
538 | + # we don't have the data for either of them, raise an error. |
539 | + try: |
540 | + status = self._bugs[actual_bug_id]['status'] |
541 | + resolution = self._bugs[actual_bug_id]['resolution'] |
542 | + except KeyError, error: |
543 | + raise UnparseableBugData |
544 | + |
545 | + if resolution != '': |
546 | + return "%s %s" % (status, resolution) |
547 | + else: |
548 | + return status |
549 | + |
550 | |
551 | class BugzillaLPPlugin(BugzillaAPI): |
552 | """An `ExternalBugTracker` to handle Bugzillas using the LP Plugin.""" |
553 | @@ -472,18 +581,6 @@ |
554 | raise BugTrackerAuthenticationError( |
555 | self.baseurl, message) |
556 | |
557 | - def _storeBugs(self, remote_bugs): |
558 | - """Store remote bugs in the local `bugs` dict.""" |
559 | - for remote_bug in remote_bugs: |
560 | - self._bugs[remote_bug['id']] = remote_bug |
561 | - |
562 | - # The bug_aliases dict is a mapping between aliases and bug |
563 | - # IDs. We use the aliases dict to look up the correct ID for |
564 | - # a bug. This allows us to reference a bug by either ID or |
565 | - # alias. |
566 | - if remote_bug['alias'] != '': |
567 | - self._bug_aliases[remote_bug['alias']] = remote_bug['id'] |
568 | - |
569 | def getModifiedRemoteBugs(self, bug_ids, last_checked): |
570 | """See `IExternalBugTracker`.""" |
571 | # We marshal last_checked into an xmlrpclib.DateTime since |
572 | @@ -538,12 +635,7 @@ |
573 | """See `IExternalBugTracker`.""" |
574 | # First, discard all those bug IDs about which we already have |
575 | # data. |
576 | - bug_ids_to_retrieve = [] |
577 | - for bug_id in bug_ids: |
578 | - try: |
579 | - actual_bug_id = self._getActualBugId(bug_id) |
580 | - except BugNotFound: |
581 | - bug_ids_to_retrieve.append(bug_id) |
582 | + bug_ids_to_retrieve = self._getBugIdsToRetrieve(bug_ids) |
583 | |
584 | # Next, grab the bugs we still need from the remote server. |
585 | # We pass permissive=True to ensure that Bugzilla won't error if |
586 | @@ -573,47 +665,6 @@ |
587 | server_utc_time = datetime(*server_timetuple[:6]) |
588 | return server_utc_time.replace(tzinfo=pytz.timezone('UTC')) |
589 | |
590 | - def _getActualBugId(self, bug_id): |
591 | - """Return the actual bug id for an alias or id.""" |
592 | - # See if bug_id is actually an alias. |
593 | - actual_bug_id = self._bug_aliases.get(bug_id) |
594 | - |
595 | - # bug_id isn't an alias, so try turning it into an int and |
596 | - # looking the bug up by ID. |
597 | - if actual_bug_id is not None: |
598 | - return actual_bug_id |
599 | - else: |
600 | - try: |
601 | - actual_bug_id = int(bug_id) |
602 | - except ValueError: |
603 | - # If bug_id can't be int()'d then it's likely an alias |
604 | - # that doesn't exist, so raise BugNotFound. |
605 | - raise BugNotFound(bug_id) |
606 | - |
607 | - # Check that the bug does actually exist. That way we're |
608 | - # treating integer bug IDs and aliases in the same way. |
609 | - if actual_bug_id not in self._bugs: |
610 | - raise BugNotFound(bug_id) |
611 | - |
612 | - return actual_bug_id |
613 | - |
614 | - def getRemoteStatus(self, bug_id): |
615 | - """See `IExternalBugTracker`.""" |
616 | - actual_bug_id = self._getActualBugId(bug_id) |
617 | - |
618 | - # Attempt to get the status and resolution from the bug. If |
619 | - # we don't have the data for either of them, raise an error. |
620 | - try: |
621 | - status = self._bugs[actual_bug_id]['status'] |
622 | - resolution = self._bugs[actual_bug_id]['resolution'] |
623 | - except KeyError, error: |
624 | - raise UnparseableBugData |
625 | - |
626 | - if resolution != '': |
627 | - return "%s %s" % (status, resolution) |
628 | - else: |
629 | - return status |
630 | - |
631 | def getRemoteProduct(self, remote_bug): |
632 | """See `IExternalBugTracker`.""" |
633 | actual_bug_id = self._getActualBugId(remote_bug) |
634 | |
635 | === modified file 'lib/lp/bugs/tests/bugzilla-xmlrpc-transport.txt' |
636 | --- lib/lp/bugs/tests/bugzilla-xmlrpc-transport.txt 2009-08-18 16:39:17 +0000 |
637 | +++ lib/lp/bugs/tests/bugzilla-xmlrpc-transport.txt 2009-08-21 09:32:57 +0000 |
638 | @@ -576,3 +576,68 @@ |
639 | Traceback (most recent call last): |
640 | ... |
641 | Fault: <Fault 300: 'The username or password you entered is not valid.'> |
642 | + |
643 | + |
644 | +Getting the current time |
645 | +------------------------ |
646 | + |
647 | +The Bugzilla.time() method allows us to retrieve a dict of the time on |
648 | +the remote server. |
649 | + |
650 | + >>> time_dict = server.Bugzilla.time() |
651 | + >>> for key in sorted(time_dict): |
652 | + ... print "%s: %s" % (key, time_dict[key]) |
653 | + db_time: 20080501T01:01:01 |
654 | + tz_name: UTC |
655 | + tz_offset: +0000 |
656 | + tz_short_name: UTC |
657 | + web_time: 20080501T01:01:01 |
658 | + web_time_utc: 20080501T01:01:01 |
659 | + |
660 | +If the remote server is in a different timezone, the db_time and |
661 | +web_time items will be in the server's local timezone whilst |
662 | +web_time_utc will be in UTC. |
663 | + |
664 | + >>> bugzilla_transport.utc_offset = 60**2 |
665 | + >>> bugzilla_transport.timezone = 'CET' |
666 | + >>> time_dict = server.Bugzilla.time() |
667 | + >>> for key in sorted(time_dict): |
668 | + ... print "%s: %s" % (key, time_dict[key]) |
669 | + db_time: 20080501T01:01:01 |
670 | + tz_name: CET |
671 | + tz_offset: +0100 |
672 | + tz_short_name: CET |
673 | + web_time: 20080501T01:01:01 |
674 | + web_time_utc: 20080501T00:01:01 |
675 | + |
676 | + |
677 | +Getting bugs from the server |
678 | +---------------------------- |
679 | + |
680 | +The Bugzilla API method Bug.get() allows us to get one or more bugs from |
681 | +the remote server. It takes a list of bug IDs to return and returns a |
682 | +list of dicts containing those bugs' data. |
683 | + |
684 | + >>> return_value = server.Bug.get( |
685 | + ... {'ids': [1], 'permissive': True}) |
686 | + >>> [bug_dict] = return_value['bugs'] |
687 | + >>> for key in sorted(bug_dict): |
688 | + ... print "%s: %s" % (key, bug_dict[key]) |
689 | + alias: |
690 | + assigned_to: test@canonical.com |
691 | + component: GPPSystems |
692 | + creation_time: 20080610T16:19:53 |
693 | + id: 1 |
694 | + internals:... |
695 | + is_open: True |
696 | + last_change_time: 20080610T16:19:53 |
697 | + priority: P1 |
698 | + product: Marvin |
699 | + resolution: FIXED |
700 | + severity: normal |
701 | + status: RESOLVED |
702 | + summary: That bloody robot still exists. |
703 | + |
704 | +Note that further tests for this functionality can be found in the |
705 | +"Launchpad.get_bugs()" section, above. This is because these two methods |
706 | +are synonymous. |
707 | |
708 | === modified file 'lib/lp/bugs/tests/externalbugtracker.py' |
709 | --- lib/lp/bugs/tests/externalbugtracker.py 2009-08-18 16:41:18 +0000 |
710 | +++ lib/lp/bugs/tests/externalbugtracker.py 2009-08-21 09:32:57 +0000 |
711 | @@ -522,7 +522,9 @@ |
712 | # what BugZilla will return. |
713 | local_time = xmlrpclib.DateTime(local_datetime.timetuple()) |
714 | |
715 | - utc_date_time = local_datetime - timedelta(seconds=self.utc_offset) |
716 | + utc_offset_delta = timedelta(seconds=self.utc_offset) |
717 | + utc_date_time = local_datetime - utc_offset_delta |
718 | + |
719 | utc_time = xmlrpclib.DateTime(utc_date_time.timetuple()) |
720 | return { |
721 | 'local_time': local_time, |
722 | @@ -757,7 +759,11 @@ |
723 | |
724 | # Map namespaces onto method names. |
725 | methods = { |
726 | - 'Bugzilla': ['version'], |
727 | + 'Bug': ['get'], |
728 | + 'Bugzilla': [ |
729 | + 'time', |
730 | + 'version', |
731 | + ], |
732 | 'Test': ['login_required'], |
733 | 'User': ['login'], |
734 | } |
735 | @@ -794,6 +800,29 @@ |
736 | 300, |
737 | "The username or password you entered is not valid.") |
738 | |
739 | + def time(self): |
740 | + """Return a dict of the local time and associated data.""" |
741 | + # We cheat slightly by calling the superclass to get the time |
742 | + # data. We do this the old fashioned way because XML-RPC |
743 | + # Transports don't support new-style classes. |
744 | + time_dict = TestBugzillaXMLRPCTransport.time(self) |
745 | + offset_hours = (self.utc_offset / 60) / 60 |
746 | + offset_string = '+%02d00' % offset_hours |
747 | + |
748 | + return { |
749 | + 'db_time': time_dict['local_time'], |
750 | + 'tz_name': time_dict['tz_name'], |
751 | + 'tz_offset': offset_string, |
752 | + 'tz_short_name': time_dict['tz_name'], |
753 | + 'web_time': time_dict['local_time'], |
754 | + 'web_time_utc': time_dict['utc_time'], |
755 | + } |
756 | + |
757 | + def get(self, arguments): |
758 | + """Return a list of bug dicts for a given set of bug ids.""" |
759 | + # This method is actually just a synonym for get_bugs(). |
760 | + return self.get_bugs(arguments) |
761 | + |
762 | |
763 | class TestMantis(Mantis): |
764 | """Mantis ExternalSystem for use in tests. |
This branch is a straight refactoring to fix bug 415774. I've moved the kers.
getRemoteStatus() method from the BugzillaLPPlugin ExternalBugTracker to
BugzillaAPI, from which BugzillaLPPlugin descends. The method needs no
alteration to work for both ExternalBugTrac
I've also moved the relevant tests into the bugzilla-api doctest.
Note that this branch depends on a previous, unlanded branch. Please use
the diff below rather than the one provided by Launchpad.
= Launchpad lint =
Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.
Linting changed files: bugs/doc/ externalbugtrac ker-bugzilla- api.txt bugs/doc/ externalbugtrac ker-bugzilla- lp-plugin. txt bugs/externalbu gtracker/ bugzilla. py
lib/lp/
lib/lp/
lib/lp/
Diff of changes
---------------
=== modified file 'lib/lp/ bugs/doc/ externalbugtrac ker-bugzilla- api.txt' bugs/doc/ externalbugtrac ker-bugzilla- api.txt 2009-08-21 09:32:57 +0000 bugs/doc/ externalbugtrac ker-bugzilla- api.txt 2009-08-21 10:51:09 +0000
--- lib/lp/
+++ lib/lp/
@@ -219,3 +219,38 @@
>>> bugzilla. _getActualBugId (2) ------- ------- --- getRemoteStatus () will return the remote status of a given print_method_ calls = False initializeRemot eBugDB( [1, 2]) getRemoteStatus (1) getRemoteStatus (2) getRemoteStatus ('no-such- bug') getRemoteStatus (999)
2
+
+
+Getting remote statuses
+------
+
+BugzillaAPI.
+bug as a string. If the bug has a resolution, that will be returned too.
+
+ >>> test_transport.
+ >>> bugzilla.
+
+ >>> print bugzilla.
+ RESOLVED FIXED
+
+ >>> print bugzilla.
+ NEW
+
+If a bug can't be found a BugNotFound error will be raised.
+
+ >>> bugzilla.
+ Traceback (most recent call last):
+ ...
+ BugNotFound: no-such-bug
+
+If the data we've imported from Bugzilla is incomplete and doesn't
+contain either the bug's status or its resolution an UnparseableBugData
+error will be raised. We can add a sample bug to demonstrate this.
+
+ >>> bugzilla._bugs[999] = {}
+ >>> bugzilla.
+ Traceback (most recent call last):
+ ...
+ UnparseableBugData
+
+ >>> del bugzilla._bugs[999]
=== modified file 'lib/lp/ bugs/doc/ externalbugtrac ker-bugzilla- lp-plugin. txt' bugs/doc/ externalbugtrac ker-bugzilla- lp-plugin. txt 2009-08-21 09:32:57 +0000 bugs/doc/ externalbugtrac ker-bugzilla- lp-plugin. txt 2009-08-21 10:42:55 +0000 ------- ------- ---
--- lib/lp/
+++ lib/lp/
@@ -301,34 +301,10 @@
Getting remote statuses
------
-BugzillaLPPlug in.getRemoteSta tus() will return the remote status of a getRemoteStatus (1) getRemoteStatus (2) getRemoteStatus ('no-such- bug')
-given bug as a string. If the bug has a resolution, that will be
-returned too.
-
- >>> print bugzilla.
- RESOLVED FIXED
-
- >>> print bugzilla.
- NEW
-
-If a bug can't be found a BugNotFound error will be raised.
-
- >>> bugzilla.
- Traceback (most recent call last):
- ...
- BugNotFound: no-such-bug
-
-If the data we've imported from Bugzilla is incomplete and doesn't
-contain either the bug's status or its resolution an UnparseableBugData
-error will be raised. We can add a sample bug to demonstrate this.
-
- >>> bugzilla._bugs[999] = {}
- >>> ...