Merge lp:~gmb/launchpad/bugzilla-3.4-sync-statuses-bug-415774 into lp:launchpad

Proposed by Graham Binns
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
Reviewer Review Type Date Requested Status
Abel Deuring (community) code Approve
Review via email: mp+10520@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) wrote :
Download full text (5.4 KiB)

This branch is a straight refactoring to fix bug 415774. I've moved the
getRemoteStatus() method from the BugzillaLPPlugin ExternalBugTracker to
BugzillaAPI, from which BugzillaLPPlugin descends. The method needs no
alteration to work for both ExternalBugTrackers.

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:
  lib/lp/bugs/doc/externalbugtracker-bugzilla-api.txt
  lib/lp/bugs/doc/externalbugtracker-bugzilla-lp-plugin.txt
  lib/lp/bugs/externalbugtracker/bugzilla.py

Diff of changes
---------------

=== modified file 'lib/lp/bugs/doc/externalbugtracker-bugzilla-api.txt'
--- lib/lp/bugs/doc/externalbugtracker-bugzilla-api.txt 2009-08-21 09:32:57 +0000
+++ lib/lp/bugs/doc/externalbugtracker-bugzilla-api.txt 2009-08-21 10:51:09 +0000
@@ -219,3 +219,38 @@

     >>> bugzilla._getActualBugId(2)
     2
+
+
+Getting remote statuses
+-----------------------
+
+BugzillaAPI.getRemoteStatus() will return the remote status of a given
+bug as a string. If the bug has a resolution, that will be returned too.
+
+ >>> test_transport.print_method_calls = False
+ >>> bugzilla.initializeRemoteBugDB([1, 2])
+
+ >>> print bugzilla.getRemoteStatus(1)
+ RESOLVED FIXED
+
+ >>> print bugzilla.getRemoteStatus(2)
+ NEW
+
+If a bug can't be found a BugNotFound error will be raised.
+
+ >>> bugzilla.getRemoteStatus('no-such-bug')
+ 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.getRemoteStatus(999)
+ Traceback (most recent call last):
+ ...
+ UnparseableBugData
+
+ >>> del bugzilla._bugs[999]

=== modified file 'lib/lp/bugs/doc/externalbugtracker-bugzilla-lp-plugin.txt'
--- lib/lp/bugs/doc/externalbugtracker-bugzilla-lp-plugin.txt 2009-08-21 09:32:57 +0000
+++ lib/lp/bugs/doc/externalbugtracker-bugzilla-lp-plugin.txt 2009-08-21 10:42:55 +0000
@@ -301,34 +301,10 @@
 Getting remote statuses
 -----------------------

-BugzillaLPPlugin.getRemoteStatus() will return the remote status of a
-given bug as a string. If the bug has a resolution, that will be
-returned too.
-
- >>> print bugzilla.getRemoteStatus(1)
- RESOLVED FIXED
-
- >>> print bugzilla.getRemoteStatus(2)
- NEW
-
-If a bug can't be found a BugNotFound error will be raised.
-
- >>> bugzilla.getRemoteStatus('no-such-bug')
- 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] = {}
- >>> ...

Read more...

Revision history for this message
Abel Deuring (adeuring) wrote :

Hi Graham,

again, no complaints except a trailing space ;)

r=me
Abel

> === modified file 'lib/lp/bugs/doc/externalbugtracker-bugzilla-lp-plugin.txt'
> --- lib/lp/bugs/doc/externalbugtracker-bugzilla-lp-plugin.txt 2009-08-21 09:32:57 +0000
> +++ lib/lp/bugs/doc/externalbugtracker-bugzilla-lp-plugin.txt 2009-08-21 10:42:55 +0000
> @@ -301,34 +301,10 @@
> Getting remote statuses
> -----------------------
>
> -BugzillaLPPlugin.getRemoteStatus() will return the remote status of a
> -given bug as a string. If the bug has a resolution, that will be
> -returned too.
> -
> - >>> print bugzilla.getRemoteStatus(1)
> - RESOLVED FIXED
> -
> - >>> print bugzilla.getRemoteStatus(2)
> - NEW
> -
> -If a bug can't be found a BugNotFound error will be raised.
> -
> - >>> bugzilla.getRemoteStatus('no-such-bug')
> - 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.getRemoteStatus(999)
> - 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.

> +externalbugtracker-bugzilla-api.txt for details of getting remote
> +statuses from Bugzilla APIs.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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.