Merge lp:~gmb/launchpad/bugzilla-3.4-init-db-bug-416514 into lp:launchpad

Proposed by Graham Binns
Status: Merged
Merged at revision: not available
Proposed branch: lp:~gmb/launchpad/bugzilla-3.4-init-db-bug-416514
Merge into: lp:launchpad
Diff against target: None lines
To merge this branch: bzr merge lp:~gmb/launchpad/bugzilla-3.4-init-db-bug-416514
Reviewer Review Type Date Requested Status
Abel Deuring (community) code Approve
Review via email: mp+10516@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) wrote :
Download full text (18.7 KiB)

This branch adds an initializeRemoteBugDB() method to the BugzillaAPI
ExternalBugTracker, in accordance with IExternalBugTracker. I've
refactored a couple of methods out of BugzillaLPPlugin, which descends
from BugzillaAPI, and into the BugzillaAPI class because they are useful
utilities.

I've also refactored the associated tests into the bugzilla-api doctest.

Note that this branch is based on a branch that hasn't yet landed (but
which is r=cprov). Please use the diff below rather than the one
generated by the merge proposal.

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
  lib/lp/bugs/tests/bugzilla-xmlrpc-transport.txt
  lib/lp/bugs/tests/externalbugtracker.py

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

=== modified file 'lib/lp/bugs/doc/externalbugtracker-bugzilla-api.txt'
--- lib/lp/bugs/doc/externalbugtracker-bugzilla-api.txt 2009-08-20 15:43:37 +0000
+++ lib/lp/bugs/doc/externalbugtracker-bugzilla-api.txt 2009-08-21 09:51:57 +0000
@@ -111,3 +111,111 @@
     >>> bugzilla.getCurrentDBTime()
     CALLED Bugzilla.time()
     datetime.datetime(2009, 8, 19, 22, 2, 2, tzinfo=<UTC>)
+
+
+Initializing the bug database
+-----------------------------
+
+BugzillaAPI implements IExternalBugTracker.initializeRemoteBugDB(),
+which takes a list of bug IDs to fetch from the remote server and stores
+those bugs locally for future use.
+
+ >>> bugzilla.initializeRemoteBugDB([1, 2])
+ CALLED Bug.get({'ids': [1, 2], 'permissive': True})
+
+The bug data is stored as a list of dicts:
+
+ >>> def print_bugs(bugs):
+ ... for bug in sorted(bugs):
+ ... print "Bug %s:" % bug
+ ... for key in sorted(bugzilla._bugs[bug]):
+ ... print " %s: %s" % (key, bugzilla._bugs[bug][key])
+ ... print "\n"
+
+ >>> print_bugs(bugzilla._bugs)
+ Bug 1:
+ alias:
+ assigned_to: <email address hidden>
+ component: GPPSystems
+ creation_time: 20080610T16:19:53
+ id: 1
+ internals:...
+ is_open: True
+ last_change_time: 20080610T16:19:53
+ priority: P1
+ product: Marvin
+ resolution: FIXED
+ severity: normal
+ status: RESOLVED
+ summary: That bloody robot still exists.
+ <BLANKLINE>
+ Bug 2:
+ alias: bug-two
+ assigned_to: <email address hidden>
+ component: Crew
+ creation_time: 20080611T09:23:12
+ id: 2
+ internals:...
+ is_open: True
+ last_change_time: 20080611T09:24:29
+ priority: P1
+ product: HeartOfGold
+ resolution:
+ severity: high
+ status: NEW
+ summary: Collect unknown persons in docking bay 2.
+ <BLANKLINE>
+ <BLANKLINE>
+
+
+Storing bugs
+------------
+
+initializeRemoteBugDB() uses the _storeBugs() method to store bug data.
+_storeBugs() will only store a bug once, even if it is requested both by
+alias and ID...

Revision history for this message
Abel Deuring (adeuring) wrote :
Download full text (4.7 KiB)

Hi Graham,

nice branch. No complaints, but I found a few lines with trailing spaces.
Could you remove them?

r=me

Abel

> === modified file 'lib/lp/bugs/doc/externalbugtracker-bugzilla-api.txt'
> --- lib/lp/bugs/doc/externalbugtracker-bugzilla-api.txt 2009-08-20 15:43:37 +0000
> +++ lib/lp/bugs/doc/externalbugtracker-bugzilla-api.txt 2009-08-21 09:51:57 +0000
> @@ -111,3 +111,111 @@
> >>> bugzilla.getCurrentDBTime()
> CALLED Bugzilla.time()
> datetime.datetime(2009, 8, 19, 22, 2, 2, tzinfo=<UTC>)
> +
> +
> +Initializing the bug database
> +-----------------------------
> +
> +BugzillaAPI implements IExternalBugTracker.initializeRemoteBugDB(),
> +which takes a list of bug IDs to fetch from the remote server and stores
> +those bugs locally for future use.
> +
> + >>> bugzilla.initializeRemoteBugDB([1, 2])
> + CALLED Bug.get({'ids': [1, 2], 'permissive': True})
> +
> +The bug data is stored as a list of dicts:
> +
> + >>> def print_bugs(bugs):
> + ... for bug in sorted(bugs):
> + ... print "Bug %s:" % bug
> + ... for key in sorted(bugzilla._bugs[bug]):
> + ... print " %s: %s" % (key, bugzilla._bugs[bug][key])
> + ... print "\n"
> +
> + >>> print_bugs(bugzilla._bugs)
> + Bug 1:
> + alias:

trailing space.

> + assigned_to: <email address hidden>
> + component: GPPSystems
> + creation_time: 20080610T16:19:53
> + id: 1
> + internals:...
> + is_open: True
> + last_change_time: 20080610T16:19:53
> + priority: P1
> + product: Marvin
> + resolution: FIXED
> + severity: normal
> + status: RESOLVED
> + summary: That bloody robot still exists.
> + <BLANKLINE>
> + Bug 2:
> + alias: bug-two
> + assigned_to: <email address hidden>
> + component: Crew
> + creation_time: 20080611T09:23:12
> + id: 2
> + internals:...
> + is_open: True
> + last_change_time: 20080611T09:24:29
> + priority: P1
> + product: HeartOfGold
> + resolution:

trailing space.

> + severity: high
> + status: NEW
> + summary: Collect unknown persons in docking bay 2.
> + <BLANKLINE>
> + <BLANKLINE>
> +
> +
> +Storing bugs
> +------------
> +
> +initializeRemoteBugDB() uses the _storeBugs() method to store bug data.
> +_storeBugs() will only store a bug once, even if it is requested both by
> +alias and ID. We'll reset the test BugzillaAPI's _bugs and _bug_aliases
> +dicts to demonstrate this.
> +
> + >>> bugzilla._bugs = {}
> + >>> bugzilla._bug_aliases = {}
> + >>> bugzilla.initializeRemoteBugDB([2, 'bug-two'])
> + CALLED Bug.get({'ids': [2, 'bug-two'], 'permissive': True})
> +
> + >>> for bug in sorted(bugzilla._bugs):
> + ... print "Bug %r:" % bug
> + ... for key in sorted(bugzilla._bugs[bug]):
> + ... print " %s: %s" % (key, bugzilla._bugs[bug][key])
> + ... print "\n"
> + Bug 2:
> + alias: bug-two
> + assigned_to: <email address hidden>
> + component: Crew
> + creation_time: 20080611T09:23:12
> + id: 2
> +...

Read more...

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 09:32:57 +0000
4@@ -75,3 +75,147 @@
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=== modified file 'lib/lp/bugs/doc/externalbugtracker-bugzilla-lp-plugin.txt'
154--- lib/lp/bugs/doc/externalbugtracker-bugzilla-lp-plugin.txt 2009-08-19 09:22:08 +0000
155+++ lib/lp/bugs/doc/externalbugtracker-bugzilla-lp-plugin.txt 2009-08-21 09:32:57 +0000
156@@ -1,4 +1,5 @@
157-= Bugzilla bugtrackers with the Launchpad plugin =
158+Bugzilla bugtrackers with the Launchpad plugin
159+==============================================
160
161 These tests cover the BugzillaLPPlugin ExternalBugTracker, which handles
162 Bugzilla instances that have the Launchpad plugin installed.
163@@ -25,7 +26,8 @@
164 True
165
166
167-== Authentication ==
168+Authentication
169+--------------
170
171 XML-RPC methods that modify data on the remote server require
172 authentication. To authenticate, we create a LoginToken of type
173@@ -154,7 +156,8 @@
174 Protocol error: 500 "Internal server error"
175
176
177-== Getting the current time ==
178+Getting the current time
179+------------------------
180
181 The BugzillaLPPlugin ExternalBugTracker, like all other
182 ExternalBugTrackers, has a getCurrentDBTime() method, which returns the
183@@ -173,7 +176,8 @@
184 datetime.datetime(2008, 5, 16, 15, 53, 20, tzinfo=<UTC>)
185
186
187-== Initializing the remote bug database ==
188+Initializing the remote bug database
189+------------------------------------
190
191 The BugzillaLPPlugin implements the standard initializeRemoteBugDB()
192 method, taking a list of the bug ids that need to be updated. It uses
193@@ -228,56 +232,13 @@
194 <BLANKLINE>
195 <BLANKLINE>
196
197-initializeRemoteBugDB() will only store a bug once, even if it is
198-requested both by alias and ID. We'll reset the test BugzillaLPPlugin's
199-_bugs and _bug_aliases dicts to demonstrate this.
200-
201- >>> bugzilla._bugs = {}
202- >>> bugzilla._bug_aliases = {}
203- >>> bugzilla.initializeRemoteBugDB([2, 'bug-two'])
204- CALLED Launchpad.get_bugs({'ids': [2, 'bug-two'], 'permissive': True})
205-
206- >>> for bug in sorted(bugzilla._bugs):
207- ... print "Bug %r:" % bug
208- ... for key in sorted(bugzilla._bugs[bug]):
209- ... print " %s: %s" % (key, bugzilla._bugs[bug][key])
210- ... print "\n"
211- Bug 2:
212- alias: bug-two
213- assigned_to: marvin@heartofgold.ship
214- component: Crew
215- creation_time: 20080611T09:23:12
216- id: 2
217- internals:...
218- is_open: True
219- last_change_time: 20080611T09:24:29
220- priority: P1
221- product: HeartOfGold
222- resolution:
223- severity: high
224- status: NEW
225- summary: Collect unknown persons in docking bay 2.
226- <BLANKLINE>
227- <BLANKLINE>
228-
229-Aliases are stored in a separate dict, which contains a mapping between
230-the alias and the bug's actual ID.
231-
232- >>> for alias, bug_id in bugzilla._bug_aliases.items():
233- ... print "%s: %s" % (alias, bug_id)
234- bug-two: 2
235-
236-The method _getActualBugId() returns the correct bug ID for a passed bug
237-ID or alias.
238-
239- >>> bugzilla._getActualBugId('bug-two')
240- 2
241-
242- >>> bugzilla._getActualBugId(2)
243- 2
244-
245-
246-== Getting a list of changed bugs ==
247+BugzillaLPPlugin.initializeRemoteBugDB() uses its _storeBugs() method to
248+store bugs. See externalbugtracker-bugzilla-api.txt for details of
249+_storeBugs().
250+
251+
252+Getting a list of changed bugs
253+------------------------------
254
255 IExternalBugTracker defines a method, getModifiedRemoteBugs(), which
256 accepts a list of bug IDs and a datetime as a parameter and returns the
257@@ -337,7 +298,8 @@
258 CALLED Launchpad.get_bugs({'ids': [3], 'permissive': True})
259
260
261-== Getting remote statuses ==
262+Getting remote statuses
263+-----------------------
264
265 BugzillaLPPlugin.getRemoteStatus() will return the remote status of a
266 given bug as a string. If the bug has a resolution, that will be
267@@ -369,7 +331,8 @@
268 >>> del bugzilla._bugs[999]
269
270
271-== Getting the remote product ==
272+Getting the remote product
273+--------------------------
274
275 getRemoteProduct() returns the product a remote bug is associated with
276 in Bugzilla.
277@@ -393,7 +356,8 @@
278 BugNotFound: 12345
279
280
281-== Retrieving remote comments ==
282+Retrieving remote comments
283+--------------------------
284
285 BugzillaLPPlugin implments the ISupportsCommentImport interface, which
286 means that we can use it to import comments from the remote Bugzilla
287@@ -435,7 +399,8 @@
288 >>> LaunchpadZopelessLayer.switchDbUser(config.checkwatches.dbuser)
289
290
291-=== getCommentIds() ===
292+getCommentIds()
293+---------------
294
295 ISupportsCommentImport.getCommentIds() is the method used to get all the
296 comment IDs for a given bug on a remote bugtracker.
297@@ -456,7 +421,8 @@
298 BugNotFound: 42
299
300
301-=== fetchComments() ===
302+fetchComments()
303+---------------
304
305 ISupportsCommentImport.fetchComments() is the method used to fetch a
306 given set of comments from the remote bugtracker. It takes a bug watch
307@@ -488,7 +454,8 @@
308 time: 20080616T13:22:29
309
310
311-=== getPosterForComment() ===
312+getPosterForComment()
313+---------------------
314
315 ISupportsCommentImport.getPosterForComment() returns a tuple of
316 (displayname, email) for the author of a remote comment.
317@@ -507,7 +474,8 @@
318 None trillian
319
320
321-=== getMessageForComment() ===
322+getMessageForComment()
323+----------------------
324
325 ISupportsCommentImport.getMessageForComment() returns a Launchpad
326 IMessage instance for a given comment. It takes a bug watch, a comment
327@@ -533,7 +501,8 @@
328 2008-06-16 13:08:08+00:00
329
330
331-== Pushing comments to remote systems ==
332+Pushing comments to remote systems
333+----------------------------------
334
335 BugzillaLPPlugin implements the ISupportsCommentPushing interface, which
336 defines the necessary methods for pushing comments to remote servers.
337@@ -583,7 +552,8 @@
338 <BLANKLINE>
339
340
341-== Linking remote bugs to Launchpad bugs ==
342+Linking remote bugs to Launchpad bugs
343+-------------------------------------
344
345 BugzillaLPPlugin implements the ISupportsBackLinking interface, which
346 provides methods to set and retrieve the Launchpad bug that links to a
347@@ -644,7 +614,8 @@
348 10
349
350
351-== Working with a specified set of Bugzilla products ==
352+Working with a specified set of Bugzilla products
353+-------------------------------------------------
354
355 BugzillaLPPlugin can be instructed to only get the data for a set of
356 bug IDs if those bugs belong to one of a given set of products.
357@@ -691,7 +662,8 @@
358 0
359
360
361-== Getting the products for a set of remote bugs ==
362+Getting the products for a set of remote bugs
363+---------------------------------------------
364
365 BugzillaLPPlugin provides a helper method, getProductsForRemoteBugs(),
366 which takes a list of bug IDs or aliases and returns the products to
367
368=== modified file 'lib/lp/bugs/externalbugtracker/bugzilla.py'
369--- lib/lp/bugs/externalbugtracker/bugzilla.py 2009-08-19 13:19:22 +0000
370+++ lib/lp/bugs/externalbugtracker/bugzilla.py 2009-08-21 09:32:57 +0000
371@@ -430,6 +430,98 @@
372 self.baseurl,
373 "Fault %s: %s" % (fault.faultCode, fault.faultString))
374
375+ def _storeBugs(self, remote_bugs):
376+ """Store remote bugs in the local `bugs` dict."""
377+ for remote_bug in remote_bugs:
378+ self._bugs[remote_bug['id']] = remote_bug
379+
380+ # The bug_aliases dict is a mapping between aliases and bug
381+ # IDs. We use the aliases dict to look up the correct ID for
382+ # a bug. This allows us to reference a bug by either ID or
383+ # alias.
384+ if remote_bug['alias'] != '':
385+ self._bug_aliases[remote_bug['alias']] = remote_bug['id']
386+
387+ def getCurrentDBTime(self):
388+ """See `IExternalBugTracker`."""
389+ time_dict = self.xmlrpc_proxy.Bugzilla.time()
390+
391+ # Convert the XML-RPC DateTime we get back into a regular Python
392+ # datetime.
393+ server_db_timetuple = time.strptime(
394+ str(time_dict['db_time']), '%Y%m%dT%H:%M:%S')
395+ server_db_datetime = datetime(*server_db_timetuple[:6])
396+
397+ # The server's DB time is the one that we want to use. However,
398+ # this may not be in UTC, so we need to convert it. Since we
399+ # can't guarantee that the timezone data returned by the server
400+ # is sane, we work out the server's offset from UTC by looking
401+ # at the difference between the web_time and the web_time_utc
402+ # values.
403+ server_web_time = time.strptime(
404+ str(time_dict['web_time']), '%Y%m%dT%H:%M:%S')
405+ server_web_datetime = datetime(*server_web_time[:6])
406+ server_web_time_utc = time.strptime(
407+ str(time_dict['web_time_utc']), '%Y%m%dT%H:%M:%S')
408+ server_web_datetime_utc = datetime(*server_web_time_utc[:6])
409+
410+ server_utc_offset = server_web_datetime - server_web_datetime_utc
411+ server_utc_datetime = server_db_datetime - server_utc_offset
412+
413+ return server_utc_datetime.replace(tzinfo=pytz.timezone('UTC'))
414+
415+ def _getActualBugId(self, bug_id):
416+ """Return the actual bug id for an alias or id."""
417+ # See if bug_id is actually an alias.
418+ actual_bug_id = self._bug_aliases.get(bug_id)
419+
420+ # bug_id isn't an alias, so try turning it into an int and
421+ # looking the bug up by ID.
422+ if actual_bug_id is not None:
423+ return actual_bug_id
424+ else:
425+ try:
426+ actual_bug_id = int(bug_id)
427+ except ValueError:
428+ # If bug_id can't be int()'d then it's likely an alias
429+ # that doesn't exist, so raise BugNotFound.
430+ raise BugNotFound(bug_id)
431+
432+ # Check that the bug does actually exist. That way we're
433+ # treating integer bug IDs and aliases in the same way.
434+ if actual_bug_id not in self._bugs:
435+ raise BugNotFound(bug_id)
436+
437+ return actual_bug_id
438+
439+ def _getBugIdsToRetrieve(self, bug_ids):
440+ """For a set of bug IDs, return those for which we have no data."""
441+ bug_ids_to_retrieve = []
442+ for bug_id in bug_ids:
443+ try:
444+ actual_bug_id = self._getActualBugId(bug_id)
445+ except BugNotFound:
446+ bug_ids_to_retrieve.append(bug_id)
447+
448+ return bug_ids_to_retrieve
449+
450+ def initializeRemoteBugDB(self, bug_ids):
451+ """See `IExternalBugTracker`."""
452+ # First, discard all those bug IDs about which we already have
453+ # data.
454+ bug_ids_to_retrieve = self._getBugIdsToRetrieve(bug_ids)
455+
456+ # Pull the bug data from the remote server. permissive=True here
457+ # prevents Bugzilla from erroring if we ask for a bug it doesn't
458+ # have.
459+ response_dict = self.xmlrpc_proxy.Bug.get({
460+ 'ids': bug_ids_to_retrieve,
461+ 'permissive': True,
462+ })
463+ remote_bugs = response_dict['bugs']
464+
465+ self._storeBugs(remote_bugs)
466+
467
468 class BugzillaLPPlugin(BugzillaAPI):
469 """An `ExternalBugTracker` to handle Bugzillas using the LP Plugin."""
470@@ -472,18 +564,6 @@
471 raise BugTrackerAuthenticationError(
472 self.baseurl, message)
473
474- def _storeBugs(self, remote_bugs):
475- """Store remote bugs in the local `bugs` dict."""
476- for remote_bug in remote_bugs:
477- self._bugs[remote_bug['id']] = remote_bug
478-
479- # The bug_aliases dict is a mapping between aliases and bug
480- # IDs. We use the aliases dict to look up the correct ID for
481- # a bug. This allows us to reference a bug by either ID or
482- # alias.
483- if remote_bug['alias'] != '':
484- self._bug_aliases[remote_bug['alias']] = remote_bug['id']
485-
486 def getModifiedRemoteBugs(self, bug_ids, last_checked):
487 """See `IExternalBugTracker`."""
488 # We marshal last_checked into an xmlrpclib.DateTime since
489@@ -538,12 +618,7 @@
490 """See `IExternalBugTracker`."""
491 # First, discard all those bug IDs about which we already have
492 # data.
493- bug_ids_to_retrieve = []
494- for bug_id in bug_ids:
495- try:
496- actual_bug_id = self._getActualBugId(bug_id)
497- except BugNotFound:
498- bug_ids_to_retrieve.append(bug_id)
499+ bug_ids_to_retrieve = self._getBugIdsToRetrieve(bug_ids)
500
501 # Next, grab the bugs we still need from the remote server.
502 # We pass permissive=True to ensure that Bugzilla won't error if
503@@ -573,30 +648,6 @@
504 server_utc_time = datetime(*server_timetuple[:6])
505 return server_utc_time.replace(tzinfo=pytz.timezone('UTC'))
506
507- def _getActualBugId(self, bug_id):
508- """Return the actual bug id for an alias or id."""
509- # See if bug_id is actually an alias.
510- actual_bug_id = self._bug_aliases.get(bug_id)
511-
512- # bug_id isn't an alias, so try turning it into an int and
513- # looking the bug up by ID.
514- if actual_bug_id is not None:
515- return actual_bug_id
516- else:
517- try:
518- actual_bug_id = int(bug_id)
519- except ValueError:
520- # If bug_id can't be int()'d then it's likely an alias
521- # that doesn't exist, so raise BugNotFound.
522- raise BugNotFound(bug_id)
523-
524- # Check that the bug does actually exist. That way we're
525- # treating integer bug IDs and aliases in the same way.
526- if actual_bug_id not in self._bugs:
527- raise BugNotFound(bug_id)
528-
529- return actual_bug_id
530-
531 def getRemoteStatus(self, bug_id):
532 """See `IExternalBugTracker`."""
533 actual_bug_id = self._getActualBugId(bug_id)
534
535=== modified file 'lib/lp/bugs/tests/bugzilla-xmlrpc-transport.txt'
536--- lib/lp/bugs/tests/bugzilla-xmlrpc-transport.txt 2009-08-18 16:39:17 +0000
537+++ lib/lp/bugs/tests/bugzilla-xmlrpc-transport.txt 2009-08-21 09:32:57 +0000
538@@ -576,3 +576,68 @@
539 Traceback (most recent call last):
540 ...
541 Fault: <Fault 300: 'The username or password you entered is not valid.'>
542+
543+
544+Getting the current time
545+------------------------
546+
547+The Bugzilla.time() method allows us to retrieve a dict of the time on
548+the remote server.
549+
550+ >>> time_dict = server.Bugzilla.time()
551+ >>> for key in sorted(time_dict):
552+ ... print "%s: %s" % (key, time_dict[key])
553+ db_time: 20080501T01:01:01
554+ tz_name: UTC
555+ tz_offset: +0000
556+ tz_short_name: UTC
557+ web_time: 20080501T01:01:01
558+ web_time_utc: 20080501T01:01:01
559+
560+If the remote server is in a different timezone, the db_time and
561+web_time items will be in the server's local timezone whilst
562+web_time_utc will be in UTC.
563+
564+ >>> bugzilla_transport.utc_offset = 60**2
565+ >>> bugzilla_transport.timezone = 'CET'
566+ >>> time_dict = server.Bugzilla.time()
567+ >>> for key in sorted(time_dict):
568+ ... print "%s: %s" % (key, time_dict[key])
569+ db_time: 20080501T01:01:01
570+ tz_name: CET
571+ tz_offset: +0100
572+ tz_short_name: CET
573+ web_time: 20080501T01:01:01
574+ web_time_utc: 20080501T00:01:01
575+
576+
577+Getting bugs from the server
578+----------------------------
579+
580+The Bugzilla API method Bug.get() allows us to get one or more bugs from
581+the remote server. It takes a list of bug IDs to return and returns a
582+list of dicts containing those bugs' data.
583+
584+ >>> return_value = server.Bug.get(
585+ ... {'ids': [1], 'permissive': True})
586+ >>> [bug_dict] = return_value['bugs']
587+ >>> for key in sorted(bug_dict):
588+ ... print "%s: %s" % (key, bug_dict[key])
589+ alias:
590+ assigned_to: test@canonical.com
591+ component: GPPSystems
592+ creation_time: 20080610T16:19:53
593+ id: 1
594+ internals:...
595+ is_open: True
596+ last_change_time: 20080610T16:19:53
597+ priority: P1
598+ product: Marvin
599+ resolution: FIXED
600+ severity: normal
601+ status: RESOLVED
602+ summary: That bloody robot still exists.
603+
604+Note that further tests for this functionality can be found in the
605+"Launchpad.get_bugs()" section, above. This is because these two methods
606+are synonymous.
607
608=== modified file 'lib/lp/bugs/tests/externalbugtracker.py'
609--- lib/lp/bugs/tests/externalbugtracker.py 2009-08-18 16:41:18 +0000
610+++ lib/lp/bugs/tests/externalbugtracker.py 2009-08-21 09:32:57 +0000
611@@ -522,7 +522,9 @@
612 # what BugZilla will return.
613 local_time = xmlrpclib.DateTime(local_datetime.timetuple())
614
615- utc_date_time = local_datetime - timedelta(seconds=self.utc_offset)
616+ utc_offset_delta = timedelta(seconds=self.utc_offset)
617+ utc_date_time = local_datetime - utc_offset_delta
618+
619 utc_time = xmlrpclib.DateTime(utc_date_time.timetuple())
620 return {
621 'local_time': local_time,
622@@ -757,7 +759,11 @@
623
624 # Map namespaces onto method names.
625 methods = {
626- 'Bugzilla': ['version'],
627+ 'Bug': ['get'],
628+ 'Bugzilla': [
629+ 'time',
630+ 'version',
631+ ],
632 'Test': ['login_required'],
633 'User': ['login'],
634 }
635@@ -794,6 +800,29 @@
636 300,
637 "The username or password you entered is not valid.")
638
639+ def time(self):
640+ """Return a dict of the local time and associated data."""
641+ # We cheat slightly by calling the superclass to get the time
642+ # data. We do this the old fashioned way because XML-RPC
643+ # Transports don't support new-style classes.
644+ time_dict = TestBugzillaXMLRPCTransport.time(self)
645+ offset_hours = (self.utc_offset / 60) / 60
646+ offset_string = '+%02d00' % offset_hours
647+
648+ return {
649+ 'db_time': time_dict['local_time'],
650+ 'tz_name': time_dict['tz_name'],
651+ 'tz_offset': offset_string,
652+ 'tz_short_name': time_dict['tz_name'],
653+ 'web_time': time_dict['local_time'],
654+ 'web_time_utc': time_dict['utc_time'],
655+ }
656+
657+ def get(self, arguments):
658+ """Return a list of bug dicts for a given set of bug ids."""
659+ # This method is actually just a synonym for get_bugs().
660+ return self.get_bugs(arguments)
661+
662
663 class TestMantis(Mantis):
664 """Mantis ExternalSystem for use in tests.