Merge lp:~bac/launchpad/bug-759467 into lp:launchpad

Proposed by Brad Crittenden
Status: Merged
Approved by: Brad Crittenden
Approved revision: no longer in the source branch.
Merged at revision: 14106
Proposed branch: lp:~bac/launchpad/bug-759467
Merge into: lp:launchpad
Diff against target: 1708 lines (+439/-234)
24 files modified
database/schema/security.cfg (+1/-0)
lib/canonical/config/schema-lazr.conf (+1/-1)
lib/canonical/database/enumcol.py (+30/-16)
lib/canonical/launchpad/doc/enumcol.txt (+39/-4)
lib/lp/bugs/browser/bugtask.py (+3/-52)
lib/lp/bugs/browser/tests/bugs-views.txt (+5/-4)
lib/lp/bugs/browser/tests/test_bugtask.py (+1/-2)
lib/lp/bugs/configure.zcml (+1/-0)
lib/lp/bugs/doc/bugtask.txt (+80/-56)
lib/lp/bugs/interfaces/bugsummary.py (+4/-4)
lib/lp/bugs/interfaces/bugtask.py (+24/-11)
lib/lp/bugs/model/bug.py (+10/-0)
lib/lp/bugs/model/bugsummary.py (+6/-2)
lib/lp/bugs/model/bugtarget.py (+4/-4)
lib/lp/bugs/model/bugtask.py (+80/-45)
lib/lp/bugs/model/tests/test_bugsummary.py (+3/-3)
lib/lp/bugs/model/tests/test_bugtask.py (+39/-8)
lib/lp/bugs/model/tests/test_bugtask_status.py (+11/-11)
lib/lp/bugs/scripts/tests/test_bugimport.py (+6/-2)
lib/lp/bugs/tests/test_bugtask_search.py (+6/-5)
lib/lp/registry/model/distribution.py (+2/-2)
lib/lp/registry/model/distributionsourcepackage.py (+2/-2)
lib/lp/scripts/garbo.py (+42/-0)
lib/lp/scripts/tests/test_garbo.py (+39/-0)
To merge this branch: bzr merge lp:~bac/launchpad/bug-759467
Reviewer Review Type Date Requested Status
Graham Binns (community) code Approve
Review via email: mp+77999@code.launchpad.net

Commit message

[r=gmb][bug=759467] Store INCOMPLETE_WITH_RESPONSE and INCOMPLETE_WITHOUT_RESPONSE for BugTask status to make queries more efficient.

Description of the change

= Summary =

Bugs marked as INCOMPLETE can be broken into two camps and we provide
searching for each: those with responses and those without
responses. The original model stored INCOMPLETE requiring complex and
expensive queries to determine whether a response had been given.

Many apologies for the size of the branch, which is significantly over
the 800 line threshold.

== Proposed fix ==

This branch changes status to store INCOMPLETE_WITH_RESPONSE or
INCOMPLETE_WITHOUT_RESPONSE directly into the database as '_status'. A
property now named 'status' maps the two into INCOMPLETE.

An hourly garbo task is included to perform migration.

== Pre-implementation notes ==

This branch was originally done by Robert but never landed due to test
failures. I've cleaned up the branch, ferreted out some ugly bugs,
and made it landable. Robert's original branch was reviewed and
approved here:

https://code.launchpad.net/~lifeless/launchpad/bug-759467/+merge/58262

I could've, perhaps should've, made that branch a pre-requisite branch
but the large number of conflicts with devel (caused by the subsequent
introduction of the BugSummary work) makes that impractical.

== Implementation details ==

As above.

== Tests ==

The impact on the bugs world is so great, all bug tests should be run:

bin/test -vvm lp.bugs

== Demo and Q/A ==

Create bugs with and without responses and see that they are found
upon searching.

= Launchpad lint =

Lot o' lint. I cleaned up some but have deferred on the rest to avoid
further inflating the size of this branch.

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/registry/model/distributionsourcepackage.py
  lib/canonical/launchpad/doc/enumcol.txt
  lib/lp/bugs/doc/bugtask.txt
  lib/canonical/config/schema-lazr.conf
  lib/lp/bugs/model/tests/test_bugsummary.py
  lib/lp/bugs/model/tests/test_bugtask.py
  lib/lp/bugs/configure.zcml
  lib/lp/bugs/browser/tests/bugs-views.txt
  database/schema/security.cfg
  lib/lp/scripts/garbo.py
  lib/lp/bugs/model/bugtarget.py
  lib/lp/scripts/tests/test_garbo.py
  lib/lp/bugs/model/bug.py
  lib/lp/bugs/interfaces/bugsummary.py
  lib/lp/bugs/browser/tests/test_bugtask.py
  lib/lp/bugs/browser/bugtask.py
  lib/lp/bugs/model/bugsummary.py
  lib/canonical/database/enumcol.py
  lib/lp/bugs/model/tests/test_bugtask_status.py
  lib/lp/bugs/model/bugtask.py
  lib/lp/registry/model/distribution.py
  lib/lp/bugs/interfaces/bugtask.py
  lib/lp/bugs/tests/test_bugtask_search.py
  lib/lp/bugs/scripts/tests/test_bugimport.py

./lib/canonical/launchpad/doc/enumcol.txt
       1: narrative uses a moin header.
     103: want exceeds 78 characters.
     114: want exceeds 78 characters.
     127: narrative has trailing whitespace.
./lib/lp/bugs/doc/bugtask.txt
       1: narrative uses a moin header.
       9: narrative uses a moin header.
      12: narrative uses a moin header.
      91: narrative uses a moin header.
      93: source exceeds 78 characters.
     160: narrative exceeds 78 characters.
     181: narrative uses a moin header.
     208: narrative uses a moin header.
     214: narrative uses a moin header.
     233: narrative uses a moin header.
     263: narrative uses a moin header.
     562: want exceeds 78 characters.
     593: want exceeds 78 characters.
     684: narrative uses a moin header.
     686: narrative exceeds 78 characters.
     687: narrative exceeds 78 characters.
     694: narrative uses a moin header.
     728: narrative exceeds 78 characters.
     771: narrative uses a moin header.
     809: narrative uses a moin header.
     825: narrative uses a moin header.
     838: narrative exceeds 78 characters.
     862: narrative uses a moin header.
     907: narrative uses a moin header.
     931: narrative uses a moin header.
     935: source has bad indentation.
     937: source has bad indentation.
     943: narrative uses a moin header.
     948: narrative exceeds 78 characters.
     987: want exceeds 78 characters.
    1079: narrative uses a moin header.
    1097: narrative uses a moin header.
    1156: narrative uses a moin header.
./lib/canonical/config/schema-lazr.conf
     592: Line exceeds 78 characters.
./lib/lp/bugs/browser/tests/bugs-views.txt
       1: narrative uses a moin header.
      14: narrative uses a moin header.
./lib/lp/bugs/scripts/tests/test_bugimport.py
      93: E303 too many blank lines (2)
     485: E302 expected 2 blank lines, found 1
     735: E301 expected 1 blank line, found 0
     756: E301 expected 1 blank line, found 0

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) wrote :

Hi Brad,

Massive props to you for tackling this. It's a great branch and I'm very happy
with it; thank you.

There are one or two comments but no show-stoppers.

[1]

138 +Sometimes its useful to serialise things from two different (but related)
139 +schemas into one table. This works if you tell the column about both enums:
140 +
141 + >>> class BarType(DBEnumeratedType):
142 + ... use_template(FooType, exclude=('TWO'))
143 + ... THREE = DBItem(3, "Three")
144 +
145 +Redefine the table with awareness of BarType:
146 +
147 + >>> class FooTest(SQLBase):
148 + ... foo = EnumCol(schema=[FooType, BarType], default=DEFAULT)

This is really cool functionality. At least, it is if you're a nerd like me.

[2]

681 + if new_status in (BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE,
682 + BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE)

It might be worth adding a INCOMPLETE_BUGTASK_STATUSES constant to
lp.bugs.interfaces.bugtask, just to make this easier. It may also be worth
adding BugTaskStatus.INCOMPLETE to that constant, but I don't know if that
would break anything.

[3]
1247 + self.query = self.store.find((BugTask, Bug),

The (BugTask, Bug) tuple should be on a new line here.

[4]

1315 + store.flush()
1316 + transaction.commit()

Are both of these necessary?

[5]

1322 + removeSecurityProxy(
1323 + without_response)._status = BugTaskStatus.INCOMPLETE

A nitpick, but it's generally preferred to write these things thus:

        removeSecurityProxy(without_response)._status = (
            BugTaskStatus.INCOMPLETE)

[6]

1331 + self.assertEqual(1,
1326 + self.assertEqual(1,

The "1," should be on a new line, otherwise some developers (i.e. me) are
inclined to miss it and confuse the actual value with the expected value.

review: Approve (code)
Revision history for this message
Brad Crittenden (bac) wrote :

Thanks for the review Graham.

[1] Like most of the interesting parts of this branch, the enumcol extension was all from Robert.

[2] I added DB_INCOMPLETE_BUGTASK_STATUSES but did not include INCOMPLETE. In the case you site, new_status can never be INCOMPLETE as it would've been previously mapped to one of these two.

[3] Done.

[4] The store.flush() was removed.

[5] Much nicer, thanks.

[6] Ditto.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'database/schema/security.cfg'
2--- database/schema/security.cfg 2011-10-03 15:22:48 +0000
3+++ database/schema/security.cfg 2011-10-05 18:55:29 +0000
4@@ -2122,6 +2122,7 @@
5 public.bugsubscriptionfiltertag = SELECT
6 public.bugsummary_rollup_journal(integer) = EXECUTE
7 public.bugtag = SELECT
8+public.bugtask = SELECT, UPDATE
9 public.bugwatch = SELECT, UPDATE
10 public.bugwatchactivity = SELECT, DELETE
11 public.codeimportevent = SELECT, DELETE
12
13=== modified file 'lib/canonical/config/schema-lazr.conf'
14--- lib/canonical/config/schema-lazr.conf 2011-10-03 14:39:59 +0000
15+++ lib/canonical/config/schema-lazr.conf 2011-10-05 18:55:29 +0000
16@@ -1484,7 +1484,7 @@
17 max_comment_size: 3200
18
19 # The number of days of inactivity required before an unassigned
20-# bugtask with the status of INCOMPLETE is expired.
21+# bugtask with the status of INCOMPLETE_WITHOUT_RESPONSE is expired.
22 # datatype: integer
23 days_before_expiration: 60
24
25
26=== modified file 'lib/canonical/database/enumcol.py'
27--- lib/canonical/database/enumcol.py 2009-06-25 05:30:52 +0000
28+++ lib/canonical/database/enumcol.py 2011-10-05 18:55:29 +0000
29@@ -16,27 +16,46 @@
30 ]
31
32
33+def check_enum_type(enum):
34+ if not issubclass(enum, DBEnumeratedType):
35+ raise TypeError(
36+ '%r must be a DBEnumeratedType: %r' % (enum, type(enum)))
37+
38+
39+def check_type(enum):
40+ if type(enum) in (list, tuple):
41+ map(check_enum_type, enum)
42+ else:
43+ check_enum_type(enum)
44+
45+
46 class DBEnumVariable(Variable):
47 """A Storm variable class representing a DBEnumeratedType."""
48 __slots__ = ("_enum",)
49
50 def __init__(self, *args, **kwargs):
51- self._enum = kwargs.pop("enum")
52- if not issubclass(self._enum, DBEnumeratedType):
53- raise TypeError(
54- '%r must be a DBEnumeratedType: %r'
55- % (self._enum, type(self._enum)))
56+ enum = kwargs.pop("enum")
57+ if type(enum) not in (list, tuple):
58+ enum = (enum,)
59+ self._enum = enum
60+ check_type(self._enum)
61 super(DBEnumVariable, self).__init__(*args, **kwargs)
62
63 def parse_set(self, value, from_db):
64 if from_db:
65- return self._enum.items[value]
66+ for enum in self._enum:
67+ try:
68+ return enum.items[value]
69+ except KeyError:
70+ pass
71+ raise KeyError('%r not in present in any of %r' % (
72+ value, self._enum))
73 else:
74 if not zope_isinstance(value, DBItem):
75 raise TypeError("Not a DBItem: %r" % (value,))
76- if self._enum != value.enum:
77- raise TypeError("DBItem from wrong type, %r != %r" % (
78- self._enum.name, value.enum.name))
79+ if value.enum not in self._enum:
80+ raise TypeError("DBItem from unknown enum, %r not in %r" % (
81+ value.enum.name, self._enum))
82 return value
83
84 def parse_get(self, value, to_db):
85@@ -56,16 +75,11 @@
86 enum = kw.pop('enum')
87 except KeyError:
88 enum = kw.pop('schema')
89- if not issubclass(enum, DBEnumeratedType):
90- raise TypeError(
91- '%r must be a DBEnumeratedType: %r' % (enum, type(enum)))
92+ check_type(enum)
93 self._kwargs = {
94- 'enum': enum
95+ 'enum': enum,
96 }
97 super(DBSchemaEnumCol, self).__init__(**kw)
98
99
100 EnumCol = DBSchemaEnumCol
101-
102-
103-
104
105=== modified file 'lib/canonical/launchpad/doc/enumcol.txt'
106--- lib/canonical/launchpad/doc/enumcol.txt 2009-04-17 10:32:16 +0000
107+++ lib/canonical/launchpad/doc/enumcol.txt 2011-10-05 18:55:29 +0000
108@@ -1,4 +1,5 @@
109-= An example of EnumCol with EnumeratedTypes =
110+An example of EnumCol with EnumeratedTypes
111+==========================================
112
113 EnumCol is a type of column that is used in SQLBase classes where the
114 database representation is an integer and the code uses an enumerated
115@@ -25,7 +26,7 @@
116 Attempting to use a normal enumerated type for an enumcol will
117 result in an error.
118
119- >>> from lazr.enum import EnumeratedType, Item
120+ >>> from lazr.enum import EnumeratedType, Item, use_template
121 >>> class PlainFooType(EnumeratedType):
122 ... """Enumerated type for the foo column."""
123 ... ONE = Item("One")
124@@ -100,7 +101,8 @@
125 >>> t.foo = AnotherType.ONE
126 Traceback (most recent call last):
127 ...
128- TypeError: DBItem from wrong type, 'FooType' != 'AnotherType'
129+ TypeError: DBItem from unknown enum, 'AnotherType' not in
130+ (<DBEnumeratedType 'FooType'>,)
131
132 The type assigned in must be the exact type, not a derived types.
133
134@@ -111,9 +113,42 @@
135 >>> t.foo = item
136 Traceback (most recent call last):
137 ...
138- TypeError: DBItem from wrong type, 'FooType' != 'DerivedType'
139+ TypeError: DBItem from unknown enum, 'DerivedType' not in
140+ (<DBEnumeratedType 'FooType'>,)
141
142 A simple way to assign in the correct item is to use the name of the derived
143 item to access the correct item from the base type.
144
145 >>> t.foo = FooType.items[item.name]
146+
147+Sometimes its useful to serialise things from two different (but related)
148+schemas into one table. This works if you tell the column about both enums:
149+
150+ >>> class BarType(DBEnumeratedType):
151+ ... use_template(FooType, exclude=('TWO'))
152+ ... THREE = DBItem(3, "Three")
153+
154+Redefine the table with awareness of BarType:
155+
156+ >>> class FooTest(SQLBase):
157+ ... foo = EnumCol(schema=[FooType, BarType], default=DEFAULT)
158+
159+We can assign items from either schema to the table;
160+
161+ >>> t = FooTest()
162+ >>> t.foo = FooType.ONE
163+ >>> t_id = t.id
164+ >>> b = FooTest()
165+ >>> b.foo = BarType.THREE
166+ >>> b_id = b.id
167+
168+And reading back from the database correctly finds things from the schemas in
169+the order given.
170+
171+ >>> from storm.store import AutoReload
172+ >>> b.foo = AutoReload
173+ >>> t.foo = AutoReload
174+ >>> b.foo == BarType.THREE
175+ True
176+ >>> t.foo == FooType.ONE
177+ True
178
179=== modified file 'lib/lp/bugs/browser/bugtask.py'
180--- lib/lp/bugs/browser/bugtask.py 2011-10-04 01:35:42 +0000
181+++ lib/lp/bugs/browser/bugtask.py 2011-10-05 18:55:29 +0000
182@@ -104,7 +104,6 @@
183 from zope.schema import Choice
184 from zope.schema.interfaces import (
185 IContextSourceBinder,
186- IList,
187 )
188 from zope.schema.vocabulary import (
189 getVocabularyRegistry,
190@@ -223,6 +222,7 @@
191 BugTaskImportance,
192 BugTaskSearchParams,
193 BugTaskStatus,
194+ BugTaskStatusSearch,
195 BugTaskStatusSearchDisplay,
196 DEFAULT_SEARCH_BUGTASK_STATUSES_FOR_DISPLAY,
197 IBugTask,
198@@ -284,6 +284,8 @@
199 BugTaskStatus.FIXRELEASED: False,
200 BugTaskStatus.UNKNOWN: False,
201 BugTaskStatus.EXPIRED: False,
202+ BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE: True,
203+ BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE: True,
204 }
205
206
207@@ -2101,57 +2103,6 @@
208 return search_filter_url
209
210
211-def getInitialValuesFromSearchParams(search_params, form_schema):
212- """Build a dictionary that can be given as initial values to
213- setUpWidgets, based on the given search params.
214-
215- >>> initial = getInitialValuesFromSearchParams(
216- ... {'status': any(*UNRESOLVED_BUGTASK_STATUSES)}, IBugTaskSearch)
217- >>> for status in initial['status']:
218- ... print status.name
219- NEW
220- INCOMPLETE
221- CONFIRMED
222- TRIAGED
223- INPROGRESS
224- FIXCOMMITTED
225-
226- >>> initial = getInitialValuesFromSearchParams(
227- ... {'status': BugTaskStatus.INVALID}, IBugTaskSearch)
228- >>> [status.name for status in initial['status']]
229- ['INVALID']
230-
231- >>> initial = getInitialValuesFromSearchParams(
232- ... {'importance': [BugTaskImportance.CRITICAL,
233- ... BugTaskImportance.HIGH]}, IBugTaskSearch)
234- >>> [importance.name for importance in initial['importance']]
235- ['CRITICAL', 'HIGH']
236-
237- >>> getInitialValuesFromSearchParams(
238- ... {'assignee': NULL}, IBugTaskSearch)
239- {'assignee': None}
240- """
241- initial = {}
242- for key, value in search_params.items():
243- if IList.providedBy(form_schema[key]):
244- if isinstance(value, any):
245- value = value.query_values
246- elif isinstance(value, (list, tuple)):
247- value = value
248- else:
249- value = [value]
250- elif value == NULL:
251- value = None
252- else:
253- # Should be safe to pass value as it is to setUpWidgets, no need
254- # to worry
255- pass
256-
257- initial[key] = value
258-
259- return initial
260-
261-
262 class BugTaskListingItem:
263 """A decorated bug task.
264
265
266=== modified file 'lib/lp/bugs/browser/tests/bugs-views.txt'
267--- lib/lp/bugs/browser/tests/bugs-views.txt 2011-06-28 15:04:29 +0000
268+++ lib/lp/bugs/browser/tests/bugs-views.txt 2011-10-05 18:55:29 +0000
269@@ -1,4 +1,5 @@
270-= The Bugs front page =
271+The Bugs front page
272+===================
273
274 The contents on the Bugs front page is driven by MaloneView. It
275 doesn't use its context for anything, so we don't have to supply one
276@@ -11,7 +12,8 @@
277 >>> bugs_view.initialize()
278
279
280-== Recently Fixed Bugs ==
281+Recently Fixed Bugs
282+-------------------
283
284 There is a list of the most recently fixed bugs on the page. This list
285 is generated by getMostRecentlyFixedBugs(), which returns the ten most
286@@ -21,7 +23,7 @@
287 >>> from lp.bugs.interfaces.bugtask import BugTaskStatus
288 >>> from lp.bugs.model.bugtask import BugTask
289 >>> [bugtask.bug.id
290- ... for bugtask in BugTask.selectBy(status=BugTaskStatus.FIXRELEASED)]
291+ ... for bugtask in BugTask.selectBy(_status=BugTaskStatus.FIXRELEASED)]
292 [8]
293 >>> for bug in bugs_view.getMostRecentlyFixedBugs():
294 ... print "%s: %s" % (bug.id, bug.title)
295@@ -120,4 +122,3 @@
296
297 >>> len(bugs_view.getMostRecentlyFixedBugs())
298 5
299-
300
301=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
302--- lib/lp/bugs/browser/tests/test_bugtask.py 2011-10-03 10:37:22 +0000
303+++ lib/lp/bugs/browser/tests/test_bugtask.py 2011-10-05 18:55:29 +0000
304@@ -546,7 +546,6 @@
305 foo_bug = self.factory.makeBug(product=product_foo)
306 bugtask_set = getUtility(IBugTaskSet)
307 bugtask_set.createTask(foo_bug, foo_bug.owner, product_bar)
308-
309 removeSecurityProxy(product_bar).active = False
310
311 request = LaunchpadTestRequest()
312@@ -685,7 +684,7 @@
313 def test_status_field_bug_task_in_status_expired(self):
314 # If a bugtask has the status Expired, this status is included
315 # in the options.
316- removeSecurityProxy(self.bug.default_bugtask).status = (
317+ removeSecurityProxy(self.bug.default_bugtask)._status = (
318 BugTaskStatus.EXPIRED)
319 login(NO_PRIVILEGE_EMAIL)
320 view = BugTaskEditView(
321
322=== modified file 'lib/lp/bugs/configure.zcml'
323--- lib/lp/bugs/configure.zcml 2011-10-05 04:14:04 +0000
324+++ lib/lp/bugs/configure.zcml 2011-10-05 18:55:29 +0000
325@@ -213,6 +213,7 @@
326 distribution
327 distroseries
328 milestone
329+ _status
330 status
331 importance
332 assignee
333
334=== modified file 'lib/lp/bugs/doc/bugtask.txt'
335--- lib/lp/bugs/doc/bugtask.txt 2011-08-01 05:25:59 +0000
336+++ lib/lp/bugs/doc/bugtask.txt 2011-10-05 18:55:29 +0000
337@@ -1,4 +1,5 @@
338-= Introduction =
339+Introduction
340+===================================
341
342 Bugs are problems in software. When a bug gets assigned to a specific
343 upstream or distro/sourcepackagename, a bug /task/ is created. In
344@@ -6,10 +7,12 @@
345 place. Where a bug has things like a title, comments and subscribers,
346 it's the bug task that tracks importance, assignee, etc.
347
348-= Working with Bug Tasks in Launchpad =
349-
350-
351-== Creating Bug Tasks ==
352+Working with Bug Tasks in Launchpad
353+===================================
354+
355+
356+Creating Bug Tasks
357+------------------
358
359 All BugTask creation and retrieval is done through an IBugTaskSet utility.
360
361@@ -88,10 +91,11 @@
362 # the connection after a ProgrammingError is raised. ARGH.
363
364
365-== Bug Task Targets ==
366+Bug Task Targets
367+----------------
368
369- >>> from lp.registry.interfaces.distributionsourcepackage import IDistributionSourcePackage
370- >>> from lp.registry.model.distributionsourcepackage import DistributionSourcePackage
371+ >>> from lp.registry.interfaces.distributionsourcepackage \
372+ ... import IDistributionSourcePackage
373
374 The "target" of an IBugTask can be one of the items in the following
375 list.
376@@ -125,11 +129,8 @@
377 * a distribution sourcepackage
378
379 >>> def get_expected_target(distro_sp_task):
380- ... expected_target = DistributionSourcePackage(
381- ... distro_sp_task.distribution,
382- ... distro_sp_task.sourcepackagename)
383 ... return distro_sp_task.target
384- ...
385+
386 >>> debian_ff_task = bugtaskset.get(4)
387 >>> IDistributionSourcePackage.providedBy(debian_ff_task.target)
388 True
389@@ -161,7 +162,8 @@
390 True
391
392 Each task has a "bugtargetdisplayname" and a "bugtargetname", strings
393-describing the site of the task. They concatenate the names of the distribution,
394+describing the site of the task. They concatenate the names of the
395+distribution,
396
397 >>> bugtask = bugtaskset.get(17)
398 >>> bugtask.bugtargetdisplayname
399@@ -182,7 +184,8 @@
400 package).
401
402
403-=== getPackageComponent ===
404+getPackageComponent
405+...................
406
407 We offer a convenience method on IBugTask which allows you to look up
408 the archive component associated to the bugtask's target. Obviously, it
409@@ -209,13 +212,15 @@
410 main
411
412
413-== Editing Bug Tasks ==
414+Editing Bug Tasks
415+-----------------
416
417 When changing status we must pass the user making the change. Some
418 statuses are restricted to Bug Supervisors only.
419
420
421-=== Upstream Bug Tasks ===
422+Upstream Bug Tasks
423+..................
424
425 To edit an upstream task, you must be logged in. Anonymous users
426 cannot edit upstream tasks.
427@@ -234,7 +239,8 @@
428 ... STATUS_FIXRELEASED, getUtility(ILaunchBag).user)
429
430
431-=== Distro and Distro Series Bug Tasks ===
432+Distro and Distro Series Bug Tasks
433+..................................
434
435 Any logged-in user can edit tasks filed on distros as long as the bug
436 is not marked private. So, as an anonymous user, we cannot edit
437@@ -264,13 +270,14 @@
438 >>> distro_series_task.transitionToAssignee(sample_person)
439
440
441-=== Conjoined Bug Tasks ===
442+Conjoined Bug Tasks
443+...................
444
445 A bugtask open on the current development series for a distro is kept
446 in sync with the "generic" bugtask for that distro, because they
447 represent the same piece of work. The same is true for product and
448 productseries tasks, when the productseries task is targeted to the
449-IProduct.developmentfocus. The following attributes are synched:
450+IProduct.developmentfocus. The following attributes are synced:
451
452 * status
453 * assignee
454@@ -563,7 +570,8 @@
455 Traceback (most recent call last):
456 ...
457 UserCannotEditBugTaskImportance:
458- User does not have sufficient permissions to edit the bug task importance.
459+ User does not have sufficient permissions to edit the
460+ bug task importance.
461
462 >>> print generic_netapplet_task.importance.title
463 Medium
464@@ -594,7 +602,8 @@
465 Traceback (most recent call last):
466 ...
467 UserCannotEditBugTaskMilestone:
468- User does not have sufficient permissions to edit the bug task milestone.
469+ User does not have sufficient permissions to edit the bug
470+ task milestone.
471
472 >>> print devel_focus_alsa_utils_task.milestone.name
473 test
474@@ -685,17 +694,19 @@
475 False
476
477
478-= Bug Privacy =
479+Bug Privacy
480+===========
481
482-A bug is either private or public. Private bugs are only visible (e.g. in search
483-listings) to explicit subscribers and Launchpad admins. Public bugs are visible
484-to anyone.
485+A bug is either private or public. Private bugs are only visible
486+(e.g. in search listings) to explicit subscribers and Launchpad
487+admins. Public bugs are visible to anyone.
488
489 >>> from zope.event import notify
490 >>> from lazr.lifecycle.event import ObjectModifiedEvent
491
492
493-== Privacy and Unprivileged Users ==
494+Privacy and Unprivileged Users
495+------------------------------
496
497 Let's log in as the user Foo Bar (to be allowed to edit bugs):
498
499@@ -729,9 +740,9 @@
500 >>> from canonical.database.sqlbase import flush_database_updates
501 >>> flush_database_updates()
502
503-If we now login as someone who was neither implicitly nor explicitly subscribed
504-to this bug, e.g. No Privileges Person, they will not be able to access or set
505-properties of the bugtask.
506+If we now login as someone who was neither implicitly nor explicitly
507+subscribed to this bug, e.g. No Privileges Person, they will not be
508+able to access or set properties of the bugtask.
509
510 >>> login("no-priv@canonical.com")
511 >>> mr_no_privs = launchbag.user
512@@ -772,7 +783,8 @@
513 [2]
514
515
516-== Open bugtask count for a given list of projects ==
517+Open bugtask count for a given list of projects
518+-----------------------------------------------
519
520 IBugTaskSet.getOpenBugTasksPerProduct() will return a dictionary
521 of product_id:count entries for bugs in an open status that
522@@ -810,7 +822,8 @@
523 product_id=20 count=3
524
525
526-== Privacy and Priviledged Users ==
527+Privacy and Priviledged Users
528+-----------------------------
529
530 Now, we'll log in as Mark Shuttleworth, who was assigned to this bug
531 when it was marked private:
532@@ -826,7 +839,8 @@
533 ... BugTaskStatus.NEW, getUtility(ILaunchBag).user)
534
535
536-== Privacy and Team Awareness ==
537+Privacy and Team Awareness
538+--------------------------
539
540 No Privileges Person can't see the private bug, because he's not a subscriber:
541
542@@ -838,8 +852,9 @@
543 [4, 5, 6]
544
545
546-But if we add No Privileges Person to the Ubuntu Team, and because the Ubuntu
547-Team *is* subscribed to the bug, No Privileges Person will see the private bug.
548+But if we add No Privileges Person to the Ubuntu Team, and because the
549+Ubuntu Team *is* subscribed to the bug, No Privileges Person will see
550+the private bug.
551
552 >>> login("mark@example.com")
553 >>> ignored = ubuntu_team.addMember(
554@@ -863,7 +878,8 @@
555 [1, 4, 5, 7, 13, 15, 15]
556
557
558-== Privacy and Launchpad Admins ==
559+Privacy and Launchpad Admins
560+----------------------------
561
562 Let's log in as Daniel Henrique Debonzi:
563
564@@ -908,7 +924,8 @@
565
566
567
568-== Sorting Bug Tasks ==
569+Sorting Bug Tasks
570+-----------------
571
572 Bug tasks need to sort in a very particular order. We want product tasks
573 first, then ubuntu tasks, then other distro-related tasks. In the
574@@ -932,25 +949,28 @@
575 Tubuntu
576
577
578-== BugTask Adaptation ==
579+BugTask Adaptation
580+------------------
581
582 An IBugTask can be adapted to an IBug.
583
584- >>> from lp.bugs.interfaces.bug import IBug
585-
586- >>> bugtask_four = bugtaskset.get(4)
587- >>> bug = IBug(bugtask_four)
588- >>> bug.title
589- u'Firefox does not support SVG'
590-
591-
592-== The targetnamecache attribute of BugTask ==
593-
594-The BugTask table has this targetnamecache attribute which stores a computed
595-value to allow us to sort and search on that value without having to do lots
596-of SQL joins. This cached value gets updated daily by the
597-update-bugtask-targetnamecaches cronscript and whenever the bugtask is changed.
598-Of course, it's also computed and set when a bugtask is created.
599+ >>> from lp.bugs.interfaces.bug import IBug
600+
601+ >>> bugtask_four = bugtaskset.get(4)
602+ >>> bug = IBug(bugtask_four)
603+ >>> bug.title
604+ u'Firefox does not support SVG'
605+
606+
607+The targetnamecache attribute of BugTask
608+----------------------------------------
609+
610+The BugTask table has this targetnamecache attribute which stores a
611+computed value to allow us to sort and search on that value without
612+having to do lots of SQL joins. This cached value gets updated daily
613+by the update-bugtask-targetnamecaches cronscript and whenever the
614+bugtask is changed. Of course, it's also computed and set when a
615+bugtask is created.
616
617 `BugTask.bugtargetdisplayname` simply returns `targetnamecache`, and
618 the latter is not exposed in `IBugTask`, so the `bugtargetdisplayname`
619@@ -988,7 +1008,8 @@
620 >>> (out, err) = process.communicate()
621
622 >>> print err
623- INFO Creating lockfile: /var/lock/launchpad-launchpad-targetnamecacheupdater.lock
624+ INFO Creating lockfile:
625+ /var/lock/launchpad-launchpad-targetnamecacheupdater.lock
626 INFO Updating targetname cache of bugtasks.
627 INFO Calculating targets.
628 INFO Will check ... targets.
629@@ -1080,7 +1101,8 @@
630 u'Mozilla Thunderbird'
631
632
633-== Target Uses Malone ==
634+Target Uses Malone
635+------------------
636
637 Bug tasks have a flag, target_uses_malone, that says whether the bugtask
638 target uses Malone as its official bugtracker.
639@@ -1098,7 +1120,8 @@
640 Tubuntu False
641
642
643-== BugTask badges ==
644+BugTask badges
645+--------------
646
647 A bug can have certain properties, which results in a badge being
648 displayed in bug listings. BugTaskSet has a method,
649@@ -1157,7 +1180,8 @@
650 has_specification: False
651
652
653-== Similar bugs ==
654+Similar bugs
655+------------
656
657 It's possible to get a list of bugs similar to the current bug by
658 accessing the similar_bugs property of its bug tasks.
659
660=== modified file 'lib/lp/bugs/interfaces/bugsummary.py'
661--- lib/lp/bugs/interfaces/bugsummary.py 2011-06-20 23:36:18 +0000
662+++ lib/lp/bugs/interfaces/bugsummary.py 2011-10-05 18:55:29 +0000
663@@ -22,7 +22,7 @@
664 from canonical.launchpad import _
665 from lp.bugs.interfaces.bugtask import (
666 BugTaskImportance,
667- BugTaskStatus,
668+ BugTaskStatusSearch,
669 )
670 from lp.registry.interfaces.distribution import IDistribution
671 from lp.registry.interfaces.distroseries import IDistroSeries
672@@ -62,7 +62,7 @@
673 milestone = Object(IMilestone, readonly=True)
674
675 status = Choice(
676- title=_('Status'), vocabulary=BugTaskStatus, readonly=True)
677+ title=_('Status'), vocabulary=BugTaskStatusSearch, readonly=True)
678 importance = Choice(
679 title=_('Importance'), vocabulary=BugTaskImportance, readonly=True)
680
681@@ -81,8 +81,8 @@
682 def getBugSummaryContextWhereClause():
683 """Return a storm clause to filter bugsummaries on this context.
684
685- This method is intentended for in-appserver use only.
686-
687+ This method is intended for in-appserver use only.
688+
689 :return: Either a storm clause to filter bugsummaries, or False if
690 there cannot be any matching bug summaries.
691 """
692
693=== modified file 'lib/lp/bugs/interfaces/bugtask.py'
694--- lib/lp/bugs/interfaces/bugtask.py 2011-09-08 22:50:59 +0000
695+++ lib/lp/bugs/interfaces/bugtask.py 2011-10-05 18:55:29 +0000
696@@ -17,6 +17,8 @@
697 'BugTaskStatus',
698 'BugTaskStatusSearch',
699 'BugTaskStatusSearchDisplay',
700+ 'DB_INCOMPLETE_BUGTASK_STATUSES',
701+ 'DB_UNRESOLVED_BUGTASK_STATUSES',
702 'DEFAULT_SEARCH_BUGTASK_STATUSES_FOR_DISPLAY',
703 'IAddBugTaskForm',
704 'IAddBugTaskWithProductCreationForm',
705@@ -196,6 +198,11 @@
706 this product or source package.
707 """)
708
709+ # INCOMPLETE is never actually stored now: INCOMPLETE_WITH_RESPONSE and
710+ # INCOMPLETE_WITHOUT_RESPONSE are mapped to INCOMPLETE on read, and on
711+ # write INCOMPLETE is mapped to INCOMPLETE_WITHOUT_RESPONSE. This permits
712+ # An index on the INCOMPLETE_WITH*_RESPONSE queries that the webapp
713+ # generates.
714 INCOMPLETE = DBItem(15, """
715 Incomplete
716
717@@ -269,10 +276,6 @@
718 affected software.
719 """)
720
721- # DBItem values 35 and 40 are used by
722- # BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE and
723- # BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE
724-
725 UNKNOWN = DBItem(999, """
726 Unknown
727
728@@ -287,19 +290,14 @@
729 """
730 use_template(BugTaskStatus, exclude=('UNKNOWN'))
731
732- sort_order = (
733- 'NEW', 'INCOMPLETE_WITH_RESPONSE', 'INCOMPLETE_WITHOUT_RESPONSE',
734- 'INCOMPLETE', 'OPINION', 'INVALID', 'WONTFIX', 'EXPIRED',
735- 'CONFIRMED', 'TRIAGED', 'INPROGRESS', 'FIXCOMMITTED', 'FIXRELEASED')
736-
737- INCOMPLETE_WITH_RESPONSE = DBItem(35, """
738+ INCOMPLETE_WITH_RESPONSE = DBItem(13, """
739 Incomplete (with response)
740
741 This bug has new information since it was last marked
742 as requiring a response.
743 """)
744
745- INCOMPLETE_WITHOUT_RESPONSE = DBItem(40, """
746+ INCOMPLETE_WITHOUT_RESPONSE = DBItem(14, """
747 Incomplete (without response)
748
749 This bug requires more information, but no additional
750@@ -371,6 +369,17 @@
751 BugTaskStatus.INPROGRESS,
752 BugTaskStatus.FIXCOMMITTED)
753
754+# Actual values stored in the DB:
755+DB_INCOMPLETE_BUGTASK_STATUSES = (
756+ BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE,
757+ BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE,
758+ )
759+
760+DB_UNRESOLVED_BUGTASK_STATUSES = (
761+ UNRESOLVED_BUGTASK_STATUSES +
762+ DB_INCOMPLETE_BUGTASK_STATUSES
763+ )
764+
765 RESOLVED_BUGTASK_STATUSES = (
766 BugTaskStatus.FIXRELEASED,
767 BugTaskStatus.OPINION,
768@@ -481,9 +490,13 @@
769 # bugwatch; this would be better described in a separate interface,
770 # but adding a marker interface during initialization is expensive,
771 # and adding it post-initialization is not trivial.
772+ # Note that status is a property because the model only exposes INCOMPLETE
773+ # but the DB stores INCOMPLETE_WITH_RESPONSE and
774+ # INCOMPLETE_WITHOUT_RESPONSE for query efficiency.
775 status = exported(
776 Choice(title=_('Status'), vocabulary=BugTaskStatus,
777 default=BugTaskStatus.NEW, readonly=True))
778+ _status = Attribute('The actual status DB column used in queries.')
779 importance = exported(
780 Choice(title=_('Importance'), vocabulary=BugTaskImportance,
781 default=BugTaskImportance.UNDECIDED, readonly=True))
782
783=== modified file 'lib/lp/bugs/model/bug.py'
784--- lib/lp/bugs/model/bug.py 2011-09-29 15:38:53 +0000
785+++ lib/lp/bugs/model/bug.py 2011-10-05 18:55:29 +0000
786@@ -150,6 +150,7 @@
787 from lp.bugs.interfaces.bugnotification import IBugNotificationSet
788 from lp.bugs.interfaces.bugtask import (
789 BugTaskStatus,
790+ BugTaskStatusSearch,
791 IBugTask,
792 IBugTaskSet,
793 UNRESOLVED_BUGTASK_STATUSES,
794@@ -1181,6 +1182,15 @@
795 getUtility(IBugWatchSet).fromText(
796 message.text_contents, self, user)
797 self.findCvesInText(message.text_contents, user)
798+ for bugtask in self.bugtasks:
799+ # Check the stored value so we don't write to unaltered tasks.
800+ if (bugtask._status in (
801+ BugTaskStatus.INCOMPLETE,
802+ BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE)):
803+ # This is not a semantic change, so we don't update date
804+ # records or send email.
805+ bugtask._status = (
806+ BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE)
807 # XXX 2008-05-27 jamesh:
808 # Ensure that BugMessages get flushed in same order as
809 # they are created.
810
811=== modified file 'lib/lp/bugs/model/bugsummary.py'
812--- lib/lp/bugs/model/bugsummary.py 2011-06-21 00:04:12 +0000
813+++ lib/lp/bugs/model/bugsummary.py 2011-10-05 18:55:29 +0000
814@@ -28,6 +28,7 @@
815 from lp.bugs.interfaces.bugtask import (
816 BugTaskImportance,
817 BugTaskStatus,
818+ BugTaskStatusSearch,
819 )
820 from lp.registry.model.distribution import Distribution
821 from lp.registry.model.distroseries import DistroSeries
822@@ -66,7 +67,9 @@
823 milestone_id = Int(name='milestone')
824 milestone = Reference(milestone_id, Milestone.id)
825
826- status = EnumCol(dbName='status', schema=BugTaskStatus)
827+ status = EnumCol(
828+ dbName='status', schema=(BugTaskStatus, BugTaskStatusSearch))
829+
830 importance = EnumCol(dbName='importance', schema=BugTaskImportance)
831
832 tag = Unicode()
833@@ -90,7 +93,8 @@
834
835 def __init__(self, *dimensions):
836 self.dimensions = map(
837- lambda x:removeSecurityProxy(x.getBugSummaryContextWhereClause()),
838+ lambda x:
839+ removeSecurityProxy(x.getBugSummaryContextWhereClause()),
840 dimensions)
841
842 def getBugSummaryContextWhereClause(self):
843
844=== modified file 'lib/lp/bugs/model/bugtarget.py'
845--- lib/lp/bugs/model/bugtarget.py 2011-06-20 23:36:18 +0000
846+++ lib/lp/bugs/model/bugtarget.py 2011-10-05 18:55:29 +0000
847@@ -54,7 +54,6 @@
848 from lp.bugs.interfaces.bugtaskfilter import simple_weight_calculator
849 from lp.bugs.model.bugtask import (
850 BugTaskSet,
851- get_bug_privacy_filter,
852 )
853 from lp.registry.interfaces.distribution import IDistribution
854 from lp.registry.interfaces.distributionsourcepackage import (
855@@ -123,7 +122,7 @@
856
857 def getBugSummaryContextWhereClause(self):
858 """Return a storm clause to filter bugsummaries on this context.
859-
860+
861 :return: Either a storm clause to filter bugsummaries, or False if
862 there cannot be any matching bug summaries.
863 """
864@@ -223,7 +222,8 @@
865 # IDistribution, IDistroSeries, IProjectGroup.
866 enable_bugfiling_duplicate_search = True
867
868- def getUsedBugTagsWithOpenCounts(self, user, tag_limit=0, include_tags=None):
869+ def getUsedBugTagsWithOpenCounts(self, user, tag_limit=0,
870+ include_tags=None):
871 """See IBugTarget."""
872 from lp.bugs.model.bug import get_bug_tags_open_count
873 return get_bug_tags_open_count(
874@@ -367,7 +367,7 @@
875 store = getUtility(IStoreSelector).get(MAIN_STORE, DEFAULT_FLAVOR)
876 target_clause = self._getOfficialTagClause()
877 return store.find(
878- OfficialBugTag, OfficialBugTag.tag==tag, target_clause).one()
879+ OfficialBugTag, OfficialBugTag.tag == tag, target_clause).one()
880
881 def addOfficialBugTag(self, tag):
882 """See `IOfficialBugTagTarget`."""
883
884=== modified file 'lib/lp/bugs/model/bugtask.py'
885--- lib/lp/bugs/model/bugtask.py 2011-09-28 03:37:44 +0000
886+++ lib/lp/bugs/model/bugtask.py 2011-10-05 18:55:29 +0000
887@@ -114,13 +114,14 @@
888 BugTaskSearchParams,
889 BugTaskStatus,
890 BugTaskStatusSearch,
891+ DB_INCOMPLETE_BUGTASK_STATUSES,
892+ DB_UNRESOLVED_BUGTASK_STATUSES,
893 IBugTask,
894 IBugTaskDelta,
895 IBugTaskSet,
896 IllegalRelatedBugTasksParams,
897 IllegalTarget,
898 RESOLVED_BUGTASK_STATUSES,
899- UNRESOLVED_BUGTASK_STATUSES,
900 UserCannotEditBugTaskAssignee,
901 UserCannotEditBugTaskImportance,
902 UserCannotEditBugTaskMilestone,
903@@ -343,9 +344,10 @@
904 if isinstance(value, PassthroughValue):
905 return value.value
906
907- # If this bugtask has no bug yet, then we are probably being
908- # instantiated.
909- if self.bug is None:
910+ # Check to see if the object is being instantiated. This test is specific
911+ # to SQLBase. Checking for specific attributes (like self.bug) is
912+ # insufficient and fragile.
913+ if self._SO_creating:
914 return value
915
916 # If this is a conjoined slave then call setattr on the master.
917@@ -446,7 +448,7 @@
918 _defaultOrder = ['distribution', 'product', 'productseries',
919 'distroseries', 'milestone', 'sourcepackagename']
920 _CONJOINED_ATTRIBUTES = (
921- "status", "importance", "assigneeID", "milestoneID",
922+ "_status", "importance", "assigneeID", "milestoneID",
923 "date_assigned", "date_confirmed", "date_inprogress",
924 "date_closed", "date_incomplete", "date_left_new",
925 "date_triaged", "date_fix_committed", "date_fix_released",
926@@ -475,9 +477,9 @@
927 dbName='milestone', foreignKey='Milestone',
928 notNull=False, default=None,
929 storm_validator=validate_conjoined_attribute)
930- status = EnumCol(
931+ _status = EnumCol(
932 dbName='status', notNull=True,
933- schema=BugTaskStatus,
934+ schema=(BugTaskStatus, BugTaskStatusSearch),
935 default=BugTaskStatus.NEW,
936 storm_validator=validate_status)
937 importance = EnumCol(
938@@ -528,6 +530,12 @@
939 dbName='targetnamecache', notNull=False, default=None)
940
941 @property
942+ def status(self):
943+ if self._status in DB_INCOMPLETE_BUGTASK_STATUSES:
944+ return BugTaskStatus.INCOMPLETE
945+ return self._status
946+
947+ @property
948 def title(self):
949 """See `IBugTask`."""
950 return 'Bug #%s in %s: "%s"' % (
951@@ -584,8 +592,7 @@
952 @property
953 def age(self):
954 """See `IBugTask`."""
955- UTC = pytz.timezone('UTC')
956- now = datetime.datetime.now(UTC)
957+ now = datetime.datetime.now(pytz.UTC)
958
959 return now - self.datecreated
960
961@@ -609,7 +616,7 @@
962 Note that this should be kept in sync with the completeness_clause
963 above.
964 """
965- return self.status in RESOLVED_BUGTASK_STATUSES
966+ return self._status in RESOLVED_BUGTASK_STATUSES
967
968 def findSimilarBugs(self, user, limit=10):
969 """See `IBugTask`."""
970@@ -878,12 +885,21 @@
971 "Only Bug Supervisors may change status to %s." % (
972 new_status.title,))
973
974- if self.status == new_status:
975+ if new_status == BugTaskStatus.INCOMPLETE:
976+ # We store INCOMPLETE as INCOMPLETE_WITHOUT_RESPONSE so that it
977+ # can be queried on efficiently.
978+ if (when is None or self.bug.date_last_message is None or
979+ when > self.bug.date_last_message):
980+ new_status = BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE
981+ else:
982+ new_status = BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE
983+
984+ if self._status == new_status:
985 # No change in the status, so nothing to do.
986 return
987
988 old_status = self.status
989- self.status = new_status
990+ self._status = new_status
991
992 if new_status == BugTaskStatus.UNKNOWN:
993 # Ensure that all status-related dates are cleared,
994@@ -901,8 +917,7 @@
995 return
996
997 if when is None:
998- UTC = pytz.timezone('UTC')
999- when = datetime.datetime.now(UTC)
1000+ when = datetime.datetime.now(pytz.UTC)
1001
1002 # Record the date of the particular kinds of transitions into
1003 # certain states.
1004@@ -957,17 +972,17 @@
1005 # Bugs can jump in and out of 'incomplete' status
1006 # and for just as long as they're marked incomplete
1007 # we keep a date_incomplete recorded for them.
1008- if new_status == BugTaskStatus.INCOMPLETE:
1009+ if new_status in DB_INCOMPLETE_BUGTASK_STATUSES:
1010 self.date_incomplete = when
1011 else:
1012 self.date_incomplete = None
1013
1014- if ((old_status in UNRESOLVED_BUGTASK_STATUSES) and
1015+ if ((old_status in DB_UNRESOLVED_BUGTASK_STATUSES) and
1016 (new_status in RESOLVED_BUGTASK_STATUSES)):
1017 self.date_closed = when
1018
1019 if ((old_status in RESOLVED_BUGTASK_STATUSES) and
1020- (new_status in UNRESOLVED_BUGTASK_STATUSES)):
1021+ (new_status in DB_UNRESOLVED_BUGTASK_STATUSES)):
1022 self.date_left_closed = when
1023
1024 # Ensure that we don't have dates recorded for state
1025@@ -975,7 +990,7 @@
1026 # workflow state. We want to ensure that, for example, a
1027 # bugtask that went New => Confirmed => New
1028 # has a dateconfirmed value of None.
1029- if new_status in UNRESOLVED_BUGTASK_STATUSES:
1030+ if new_status in DB_UNRESOLVED_BUGTASK_STATUSES:
1031 self.date_closed = None
1032
1033 if new_status < BugTaskStatus.CONFIRMED:
1034@@ -1591,7 +1606,7 @@
1035 """See `IBugTaskSet`."""
1036 return BugTaskSearchParams(
1037 user=getUtility(ILaunchBag).user,
1038- status=any(*UNRESOLVED_BUGTASK_STATUSES),
1039+ status=any(*DB_UNRESOLVED_BUGTASK_STATUSES),
1040 omit_dupes=True)
1041
1042 def get(self, task_id):
1043@@ -1718,29 +1733,45 @@
1044 elif zope_isinstance(status, not_equals):
1045 return '(NOT %s)' % self._buildStatusClause(status.value)
1046 elif zope_isinstance(status, BaseItem):
1047+ incomplete_response = (
1048+ status == BugTaskStatus.INCOMPLETE)
1049 with_response = (
1050 status == BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE)
1051 without_response = (
1052 status == BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE)
1053+ # TODO: bug 759467 tracks the migration of INCOMPLETE in the db to
1054+ # INCOMPLETE_WITH_RESPONSE and INCOMPLETE_WITHOUT_RESPONSE. When
1055+ # the migration is complete, we can convert status lookups to a
1056+ # simple IN clause.
1057 if with_response or without_response:
1058- status_clause = (
1059- '(BugTask.status = %s) ' %
1060- sqlvalues(BugTaskStatus.INCOMPLETE))
1061 if with_response:
1062- status_clause += ("""
1063+ return """(
1064+ BugTask.status = %s OR
1065+ (BugTask.status = %s
1066 AND (Bug.date_last_message IS NOT NULL
1067 AND BugTask.date_incomplete <=
1068- Bug.date_last_message)
1069- """)
1070+ Bug.date_last_message)))
1071+ """ % sqlvalues(
1072+ BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE,
1073+ BugTaskStatus.INCOMPLETE)
1074 elif without_response:
1075- status_clause += ("""
1076+ return """(
1077+ BugTask.status = %s OR
1078+ (BugTask.status = %s
1079 AND (Bug.date_last_message IS NULL
1080 OR BugTask.date_incomplete >
1081- Bug.date_last_message)
1082- """)
1083- else:
1084- assert with_response != without_response
1085- return status_clause
1086+ Bug.date_last_message)))
1087+ """ % sqlvalues(
1088+ BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE,
1089+ BugTaskStatus.INCOMPLETE)
1090+ assert with_response != without_response
1091+ elif incomplete_response:
1092+ # search for any of INCOMPLETE (being migrated from),
1093+ # INCOMPLETE_WITH_RESPONSE or INCOMPLETE_WITHOUT_RESPONSE
1094+ return 'BugTask.status %s' % search_value_to_where_condition(
1095+ any(BugTaskStatus.INCOMPLETE,
1096+ BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE,
1097+ BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE))
1098 else:
1099 return '(BugTask.status = %s)' % sqlvalues(status)
1100 else:
1101@@ -1777,7 +1808,7 @@
1102 And(ConjoinedMaster.bugID == BugTask.bugID,
1103 BugTask.distributionID == milestone.distribution.id,
1104 ConjoinedMaster.distroseriesID == current_series.id,
1105- Not(ConjoinedMaster.status.is_in(
1106+ Not(ConjoinedMaster._status.is_in(
1107 BugTask._NON_CONJOINED_STATUSES))))
1108 join_tables = [(ConjoinedMaster, join)]
1109 else:
1110@@ -1797,7 +1828,7 @@
1111 And(ConjoinedMaster.bugID == BugTask.bugID,
1112 ConjoinedMaster.productseriesID
1113 == Product.development_focusID,
1114- Not(ConjoinedMaster.status.is_in(
1115+ Not(ConjoinedMaster._status.is_in(
1116 BugTask._NON_CONJOINED_STATUSES)))),
1117 ]
1118 # join.right is the table name.
1119@@ -1810,7 +1841,7 @@
1120 And(ConjoinedMaster.bugID == BugTask.bugID,
1121 BugTask.productID == milestone.product.id,
1122 ConjoinedMaster.productseriesID == dev_focus_id,
1123- Not(ConjoinedMaster.status.is_in(
1124+ Not(ConjoinedMaster._status.is_in(
1125 BugTask._NON_CONJOINED_STATUSES))))
1126 join_tables = [(ConjoinedMaster, join)]
1127 else:
1128@@ -2302,6 +2333,8 @@
1129 statuses_for_open_tasks = [
1130 BugTaskStatus.NEW,
1131 BugTaskStatus.INCOMPLETE,
1132+ BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE,
1133+ BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE,
1134 BugTaskStatus.CONFIRMED,
1135 BugTaskStatus.INPROGRESS,
1136 BugTaskStatus.UNKNOWN]
1137@@ -2636,7 +2669,7 @@
1138 conditions = []
1139 # Open bug statuses
1140 conditions.append(
1141- BugSummary.status.is_in(UNRESOLVED_BUGTASK_STATUSES))
1142+ BugSummary.status.is_in(DB_UNRESOLVED_BUGTASK_STATUSES))
1143 # BugSummary does not include duplicates so no need to exclude.
1144 context_conditions = []
1145 for context in contexts:
1146@@ -2719,7 +2752,6 @@
1147 validate_new_target(bug, target)
1148
1149 target_key = bug_target_to_key(target)
1150-
1151 if not bug.private and bug.security_related:
1152 product = target_key['product']
1153 distribution = target_key['distribution']
1154@@ -2730,7 +2762,7 @@
1155
1156 non_target_create_params = dict(
1157 bug=bug,
1158- status=status,
1159+ _status=status,
1160 importance=importance,
1161 assignee=assignee,
1162 owner=owner,
1163@@ -2738,7 +2770,6 @@
1164 create_params = non_target_create_params.copy()
1165 create_params.update(target_key)
1166 bugtask = BugTask(**create_params)
1167-
1168 if target_key['distribution']:
1169 # Create tasks for accepted nominations if this is a source
1170 # package addition.
1171@@ -2862,14 +2893,16 @@
1172 """ + target_clause + """
1173 """ + bug_clause + """
1174 """ + bug_privacy_filter + """
1175- AND BugTask.status = %s
1176+ AND BugTask.status in (%s, %s, %s)
1177 AND BugTask.assignee IS NULL
1178 AND BugTask.milestone IS NULL
1179 AND Bug.duplicateof IS NULL
1180 AND Bug.date_last_updated < CURRENT_TIMESTAMP
1181 AT TIME ZONE 'UTC' - interval '%s days'
1182 AND BugWatch.id IS NULL
1183- )""" % sqlvalues(BugTaskStatus.INCOMPLETE, min_days_old)
1184+ )""" % sqlvalues(BugTaskStatus.INCOMPLETE,
1185+ BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE,
1186+ BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE, min_days_old)
1187 expirable_bugtasks = BugTask.select(
1188 query + unconfirmed_bug_condition,
1189 clauseTables=['Bug'],
1190@@ -2887,6 +2920,7 @@
1191 """
1192 statuses_not_preventing_expiration = [
1193 BugTaskStatus.INVALID, BugTaskStatus.INCOMPLETE,
1194+ BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE,
1195 BugTaskStatus.WONTFIX]
1196
1197 unexpirable_status_list = [
1198@@ -3032,9 +3066,10 @@
1199 ]
1200
1201 product_ids = [product.id for product in products]
1202- conditions = And(BugTask.status.is_in(UNRESOLVED_BUGTASK_STATUSES),
1203- Bug.duplicateof == None,
1204- BugTask.productID.is_in(product_ids))
1205+ conditions = And(
1206+ BugTask._status.is_in(DB_UNRESOLVED_BUGTASK_STATUSES),
1207+ Bug.duplicateof == None,
1208+ BugTask.productID.is_in(product_ids))
1209
1210 privacy_filter = get_bug_privacy_filter(user)
1211 if privacy_filter != '':
1212@@ -3060,7 +3095,7 @@
1213 # TODO: sort by their name?
1214 "assignee": BugTask.assigneeID,
1215 "targetname": BugTask.targetnamecache,
1216- "status": BugTask.status,
1217+ "status": BugTask._status,
1218 "title": Bug.title,
1219 "milestone": BugTask.milestoneID,
1220 "dateassigned": BugTask.date_assigned,
1221@@ -3167,7 +3202,7 @@
1222
1223 open_bugs_cond = (
1224 'BugTask.status %s' % search_value_to_where_condition(
1225- any(*UNRESOLVED_BUGTASK_STATUSES)))
1226+ any(*DB_UNRESOLVED_BUGTASK_STATUSES)))
1227
1228 sum_template = "SUM(CASE WHEN %s THEN 1 ELSE 0 END) AS %s"
1229 sums = [
1230
1231=== modified file 'lib/lp/bugs/model/tests/test_bugsummary.py'
1232--- lib/lp/bugs/model/tests/test_bugsummary.py 2011-09-22 01:45:12 +0000
1233+++ lib/lp/bugs/model/tests/test_bugsummary.py 2011-10-05 18:55:29 +0000
1234@@ -189,7 +189,7 @@
1235 for count in range(3):
1236 bug = self.factory.makeBug(product=product)
1237 bug_task = self.store.find(BugTask, bug=bug).one()
1238- bug_task.status = org_status
1239+ bug_task._status = org_status
1240
1241 self.assertEqual(
1242 self.getPublicCount(
1243@@ -199,8 +199,8 @@
1244
1245 for count in reversed(range(3)):
1246 bug_task = self.store.find(
1247- BugTask, product=product, status=org_status).any()
1248- bug_task.status = new_status
1249+ BugTask, product=product, _status=org_status).any()
1250+ bug_task._status = new_status
1251 self.assertEqual(
1252 self.getPublicCount(
1253 BugSummary.product == product,
1254
1255=== modified file 'lib/lp/bugs/model/tests/test_bugtask.py'
1256--- lib/lp/bugs/model/tests/test_bugtask.py 2011-09-28 03:07:12 +0000
1257+++ lib/lp/bugs/model/tests/test_bugtask.py 2011-10-05 18:55:29 +0000
1258@@ -32,6 +32,7 @@
1259 BugTaskImportance,
1260 BugTaskSearchParams,
1261 BugTaskStatus,
1262+ DB_UNRESOLVED_BUGTASK_STATUSES,
1263 IBugTaskSet,
1264 RESOLVED_BUGTASK_STATUSES,
1265 UNRESOLVED_BUGTASK_STATUSES,
1266@@ -1349,6 +1350,8 @@
1267 """
1268 self.assertNotIn(BugTaskStatus.UNKNOWN, RESOLVED_BUGTASK_STATUSES)
1269 self.assertNotIn(BugTaskStatus.UNKNOWN, UNRESOLVED_BUGTASK_STATUSES)
1270+ self.assertNotIn(
1271+ BugTaskStatus.UNKNOWN, DB_UNRESOLVED_BUGTASK_STATUSES)
1272
1273
1274 class TestBugTaskContributor(TestCaseWithFactory):
1275@@ -1392,26 +1395,35 @@
1276 self.owner = self.factory.makePerson()
1277 self.distro = self.factory.makeDistribution(
1278 name="eggs", owner=self.owner, bug_supervisor=self.owner)
1279- distro_release = self.factory.makeDistroSeries(
1280+ self.distro_release = self.factory.makeDistroSeries(
1281 distribution=self.distro, registrant=self.owner)
1282- source_package = self.factory.makeSourcePackage(
1283- sourcepackagename="spam", distroseries=distro_release)
1284- bug = self.factory.makeBug(
1285+ self.source_package = self.factory.makeSourcePackage(
1286+ sourcepackagename="spam", distroseries=self.distro_release)
1287+ self.bug = self.factory.makeBug(
1288 distribution=self.distro,
1289- sourcepackagename=source_package.sourcepackagename,
1290+ sourcepackagename=self.source_package.sourcepackagename,
1291 owner=self.owner)
1292 with person_logged_in(self.owner):
1293- nomination = bug.addNomination(self.owner, distro_release)
1294+ nomination = self.bug.addNomination(
1295+ self.owner, self.distro_release)
1296 nomination.approve(self.owner)
1297- self.generic_task, self.series_task = bug.bugtasks
1298+ self.generic_task, self.series_task = self.bug.bugtasks
1299
1300 def test_editing_generic_status_reflects_upon_conjoined_master(self):
1301 # If a change is made to the status of a conjoined slave
1302 # (generic) task, that change is reflected upon the conjoined
1303 # master.
1304 with person_logged_in(self.owner):
1305+ # Both the generic task and the series task start off with the
1306+ # status of NEW.
1307+ self.assertEqual(
1308+ BugTaskStatus.NEW, self.generic_task.status)
1309+ self.assertEqual(
1310+ BugTaskStatus.NEW, self.series_task.status)
1311+ # Transitioning the generic task to CONFIRMED.
1312 self.generic_task.transitionToStatus(
1313 BugTaskStatus.CONFIRMED, self.owner)
1314+ # Also transitions the series_task.
1315 self.assertEqual(
1316 BugTaskStatus.CONFIRMED, self.series_task.status)
1317
1318@@ -1448,11 +1460,30 @@
1319 self.assertEqual(
1320 source_package_name, self.series_task.sourcepackagename)
1321
1322+ def test_creating_conjoined_task_gets_synced_attributes(self):
1323+ bug = self.factory.makeBug(
1324+ distribution=self.distro,
1325+ sourcepackagename=self.source_package.sourcepackagename,
1326+ owner=self.owner)
1327+ generic_task = bug.bugtasks[0]
1328+ bugtaskset = getUtility(IBugTaskSet)
1329+ with person_logged_in(self.owner):
1330+ generic_task.transitionToStatus(
1331+ BugTaskStatus.CONFIRMED, self.owner)
1332+ self.assertEqual(
1333+ BugTaskStatus.CONFIRMED, generic_task.status)
1334+ slave_bugtask = bugtaskset.createTask(
1335+ bug, self.owner, generic_task.target.development_version)
1336+ self.assertEqual(
1337+ BugTaskStatus.CONFIRMED, generic_task.status)
1338+ self.assertEqual(
1339+ BugTaskStatus.CONFIRMED, slave_bugtask.status)
1340+
1341+
1342 # START TEMPORARY BIT FOR BUGTASK AUTOCONFIRM FEATURE FLAG.
1343 # When feature flag code is removed, delete these tests (up to "# END
1344 # TEMPORARY BIT FOR BUGTASK AUTOCONFIRM FEATURE FLAG.")
1345
1346-
1347 class TestAutoConfirmBugTasksFlagForProduct(TestCaseWithFactory):
1348 """Tests for auto-confirming bug tasks."""
1349 # Tests for _checkAutoconfirmFeatureFlag.
1350
1351=== modified file 'lib/lp/bugs/model/tests/test_bugtask_status.py'
1352--- lib/lp/bugs/model/tests/test_bugtask_status.py 2011-05-27 21:12:25 +0000
1353+++ lib/lp/bugs/model/tests/test_bugtask_status.py 2011-10-05 18:55:29 +0000
1354@@ -67,7 +67,7 @@
1355 def test_user_cannot_unset_wont_fix_status(self):
1356 # A regular user should not be able to transition a bug away
1357 # from Won't Fix.
1358- removeSecurityProxy(self.task).status = BugTaskStatus.WONTFIX
1359+ removeSecurityProxy(self.task)._status = BugTaskStatus.WONTFIX
1360 with person_logged_in(self.user):
1361 self.assertRaises(
1362 UserCannotEditBugTaskStatus, self.task.transitionToStatus,
1363@@ -76,7 +76,7 @@
1364 def test_user_cannot_unset_fix_released_status(self):
1365 # A regular user should not be able to transition a bug away
1366 # from Fix Released.
1367- removeSecurityProxy(self.task).status = BugTaskStatus.FIXRELEASED
1368+ removeSecurityProxy(self.task)._status = BugTaskStatus.FIXRELEASED
1369 with person_logged_in(self.user):
1370 self.assertRaises(
1371 UserCannotEditBugTaskStatus, self.task.transitionToStatus,
1372@@ -132,7 +132,7 @@
1373 def test_user_canTransitionToStatus_from_wontfix(self):
1374 # A regular user cannot transition away from Won't Fix,
1375 # so canTransitionToStatus should return False.
1376- removeSecurityProxy(self.task).status = BugTaskStatus.WONTFIX
1377+ removeSecurityProxy(self.task)._status = BugTaskStatus.WONTFIX
1378 self.assertEqual(
1379 self.task.canTransitionToStatus(
1380 BugTaskStatus.NEW, self.user),
1381@@ -141,7 +141,7 @@
1382 def test_user_canTransitionToStatus_from_fixreleased(self):
1383 # A regular user cannot transition away from Fix Released,
1384 # so canTransitionToStatus should return False.
1385- removeSecurityProxy(self.task).status = BugTaskStatus.FIXRELEASED
1386+ removeSecurityProxy(self.task)._status = BugTaskStatus.FIXRELEASED
1387 self.assertEqual(
1388 self.task.canTransitionToStatus(
1389 BugTaskStatus.NEW, self.user),
1390@@ -160,7 +160,7 @@
1391
1392 def test_reporter_can_unset_fix_released_status(self):
1393 # The bug reporter can transition away from Fix Released.
1394- removeSecurityProxy(self.task).status = BugTaskStatus.FIXRELEASED
1395+ removeSecurityProxy(self.task)._status = BugTaskStatus.FIXRELEASED
1396 with person_logged_in(self.reporter):
1397 self.task.transitionToStatus(
1398 BugTaskStatus.CONFIRMED, self.reporter)
1399@@ -169,7 +169,7 @@
1400 def test_reporter_canTransitionToStatus(self):
1401 # The bug reporter can transition away from Fix Released, so
1402 # canTransitionToStatus should always return True.
1403- removeSecurityProxy(self.task).status = BugTaskStatus.FIXRELEASED
1404+ removeSecurityProxy(self.task)._status = BugTaskStatus.FIXRELEASED
1405 self.assertEqual(
1406 self.task.canTransitionToStatus(
1407 BugTaskStatus.CONFIRMED, self.reporter),
1408@@ -181,7 +181,7 @@
1409 team = self.factory.makeTeam(members=[self.reporter])
1410 team_bug = self.factory.makeBug(owner=team)
1411 naked_task = removeSecurityProxy(team_bug.default_bugtask)
1412- naked_task.status = BugTaskStatus.FIXRELEASED
1413+ naked_task._status = BugTaskStatus.FIXRELEASED
1414 with person_logged_in(self.reporter):
1415 team_bug.default_bugtask.transitionToStatus(
1416 BugTaskStatus.CONFIRMED, self.reporter)
1417@@ -242,14 +242,14 @@
1418
1419 def test_privileged_user_can_unset_wont_fix_status(self):
1420 # Privileged users can transition away from Won't Fix.
1421- removeSecurityProxy(self.task).status = BugTaskStatus.WONTFIX
1422+ removeSecurityProxy(self.task)._status = BugTaskStatus.WONTFIX
1423 with person_logged_in(self.person):
1424 self.task.transitionToStatus(BugTaskStatus.CONFIRMED, self.person)
1425 self.assertEqual(self.task.status, BugTaskStatus.CONFIRMED)
1426
1427 def test_privileged_user_can_unset_fix_released_status(self):
1428 # Privileged users can transition away from Fix Released.
1429- removeSecurityProxy(self.task).status = BugTaskStatus.FIXRELEASED
1430+ removeSecurityProxy(self.task)._status = BugTaskStatus.FIXRELEASED
1431 with person_logged_in(self.person):
1432 self.task.transitionToStatus(BugTaskStatus.CONFIRMED, self.person)
1433 self.assertEqual(self.task.status, BugTaskStatus.CONFIRMED)
1434@@ -306,7 +306,7 @@
1435 def test_privileged_user_canTransitionToStatus_from_wontfix(self):
1436 # A privileged user can transition away from Won't Fix, so
1437 # canTransitionToStatus should return True.
1438- removeSecurityProxy(self.task).status = BugTaskStatus.WONTFIX
1439+ removeSecurityProxy(self.task)._status = BugTaskStatus.WONTFIX
1440 self.assertEqual(
1441 self.task.canTransitionToStatus(
1442 BugTaskStatus.NEW, self.person),
1443@@ -315,7 +315,7 @@
1444 def test_privileged_user_canTransitionToStatus_from_fixreleased(self):
1445 # A privileged user can transition away from Fix Released, so
1446 # canTransitionToStatus should return True.
1447- removeSecurityProxy(self.task).status = BugTaskStatus.FIXRELEASED
1448+ removeSecurityProxy(self.task)._status = BugTaskStatus.FIXRELEASED
1449 self.assertEqual(
1450 self.task.canTransitionToStatus(
1451 BugTaskStatus.NEW, self.person),
1452
1453=== modified file 'lib/lp/bugs/scripts/tests/test_bugimport.py'
1454--- lib/lp/bugs/scripts/tests/test_bugimport.py 2011-08-12 11:19:40 +0000
1455+++ lib/lp/bugs/scripts/tests/test_bugimport.py 2011-10-05 18:55:29 +0000
1456@@ -89,7 +89,6 @@
1457 self.assertRaises(bugimport.BugXMLSyntaxError,
1458 bugimport.get_text, node)
1459
1460-
1461 def test_get_enum_value(self):
1462 # Test that the get_enum_value() function returns the
1463 # appropriate enum value, or raises BugXMLSyntaxError if it is
1464@@ -482,6 +481,7 @@
1465 </comment>
1466 </bug>'''
1467
1468+
1469 class ImportBugTestCase(unittest.TestCase):
1470 """Test importing of a bug from XML"""
1471 layer = LaunchpadZopelessLayer
1472@@ -732,9 +732,11 @@
1473 fp.write('</launchpad-bugs>\n')
1474 fp.close()
1475 cache_filename = os.path.join(self.tmpdir, 'bug-map.pickle')
1476+
1477 class MyBugImporter(bugimport.BugImporter):
1478 def importBug(self, bugnode):
1479 raise bugnode.BugXMLSyntaxError('not imported')
1480+
1481 importer = MyBugImporter(product, xml_file, cache_filename)
1482 importer.importBugs(self.layer.txn)
1483 importer.loadCache()
1484@@ -753,9 +755,11 @@
1485 fp.close()
1486 cache_filename = os.path.join(self.tmpdir, 'bug-map.pickle')
1487 fail = self.fail
1488+
1489 class MyBugImporter(bugimport.BugImporter):
1490 def importBug(self, bugnode):
1491 fail('Should not have imported bug')
1492+
1493 importer = MyBugImporter(product, xml_file, cache_filename)
1494 # Mark the bug as imported
1495 importer.bug_id_map = {42: 1}
1496@@ -845,7 +849,7 @@
1497 if bugtask.conjoined_master is not None:
1498 continue
1499 bugtask = removeSecurityProxy(bugtask)
1500- bugtask.status = new_malone_status
1501+ bugtask._status = new_malone_status
1502 if self.failing:
1503 cur = cursor()
1504 cur.execute("""
1505
1506=== modified file 'lib/lp/bugs/tests/test_bugtask_search.py'
1507--- lib/lp/bugs/tests/test_bugtask_search.py 2011-09-27 07:53:02 +0000
1508+++ lib/lp/bugs/tests/test_bugtask_search.py 2011-10-05 18:55:29 +0000
1509@@ -75,18 +75,19 @@
1510
1511 def test_aggregate_by_target(self):
1512 # BugTaskSet.search supports returning the counts for each target (as
1513- # long only one type of target was selected).
1514+ # long as only one type of target was selected).
1515 if self.group_on is None:
1516 # Not a useful/valid permutation.
1517 return
1518 self.getBugTaskSearchParams(user=None, multitarget=True)
1519 # The test data has 3 bugs for searchtarget and 6 for searchtarget2.
1520+ user = self.factory.makePerson()
1521 expected = {(self.targetToGroup(self.searchtarget),): 3,
1522 (self.targetToGroup(self.searchtarget2),): 6}
1523- user = self.factory.makePerson()
1524- self.assertEqual(expected, self.bugtask_set.countBugs(user,
1525- (self.searchtarget, self.searchtarget2),
1526- group_on=self.group_on))
1527+ actual = self.bugtask_set.countBugs(
1528+ user, (self.searchtarget, self.searchtarget2),
1529+ group_on=self.group_on)
1530+ self.assertEqual(expected, actual)
1531
1532 def test_search_all_bugtasks_for_target(self):
1533 # BugTaskSet.search() returns all bug tasks for a given bug
1534
1535=== modified file 'lib/lp/registry/model/distribution.py'
1536--- lib/lp/registry/model/distribution.py 2011-09-28 23:31:38 +0000
1537+++ lib/lp/registry/model/distribution.py 2011-10-05 18:55:29 +0000
1538@@ -98,7 +98,7 @@
1539 from lp.bugs.interfaces.bugtarget import IHasBugHeat
1540 from lp.bugs.interfaces.bugtask import (
1541 BugTaskStatus,
1542- UNRESOLVED_BUGTASK_STATUSES,
1543+ DB_UNRESOLVED_BUGTASK_STATUSES,
1544 )
1545 from lp.bugs.interfaces.bugtaskfilter import OrderedBugTask
1546 from lp.bugs.model.bug import (
1547@@ -1557,7 +1557,7 @@
1548 'triaged': quote(BugTaskStatus.TRIAGED),
1549 'limit': limit,
1550 'distro': self.id,
1551- 'unresolved': quote(UNRESOLVED_BUGTASK_STATUSES),
1552+ 'unresolved': quote(DB_UNRESOLVED_BUGTASK_STATUSES),
1553 'excluded_packages': quote(exclude_packages),
1554 })
1555 counts = cur.fetchall()
1556
1557=== modified file 'lib/lp/registry/model/distributionsourcepackage.py'
1558--- lib/lp/registry/model/distributionsourcepackage.py 2011-10-04 21:43:11 +0000
1559+++ lib/lp/registry/model/distributionsourcepackage.py 2011-10-05 18:55:29 +0000
1560@@ -38,7 +38,7 @@
1561 from canonical.launchpad.interfaces.lpstorm import IStore
1562 from lp.bugs.interfaces.bugsummary import IBugSummaryDimension
1563 from lp.bugs.interfaces.bugtarget import IHasBugHeat
1564-from lp.bugs.interfaces.bugtask import UNRESOLVED_BUGTASK_STATUSES
1565+from lp.bugs.interfaces.bugtask import DB_UNRESOLVED_BUGTASK_STATUSES
1566 from lp.bugs.model.bug import (
1567 Bug,
1568 BugSet,
1569@@ -234,7 +234,7 @@
1570 BugTask.distributionID == self.distribution.id,
1571 BugTask.sourcepackagenameID == self.sourcepackagename.id,
1572 Bug.duplicateof == None,
1573- BugTask.status.is_in(UNRESOLVED_BUGTASK_STATUSES)).one()
1574+ BugTask._status.is_in(DB_UNRESOLVED_BUGTASK_STATUSES)).one()
1575
1576 # Aggregate functions return NULL if zero rows match.
1577 row = list(row)
1578
1579=== modified file 'lib/lp/scripts/garbo.py'
1580--- lib/lp/scripts/garbo.py 2011-10-03 09:38:06 +0000
1581+++ lib/lp/scripts/garbo.py 2011-10-05 18:55:29 +0000
1582@@ -59,9 +59,14 @@
1583 )
1584 from lp.answers.model.answercontact import AnswerContact
1585 from lp.bugs.interfaces.bug import IBugSet
1586+from lp.bugs.interfaces.bugtask import (
1587+ BugTaskStatus,
1588+ BugTaskStatusSearch,
1589+ )
1590 from lp.bugs.model.bug import Bug
1591 from lp.bugs.model.bugattachment import BugAttachment
1592 from lp.bugs.model.bugnotification import BugNotification
1593+from lp.bugs.model.bugtask import BugTask
1594 from lp.bugs.model.bugwatch import BugWatchActivity
1595 from lp.bugs.scripts.checkwatches.scheduler import (
1596 BugWatchScheduler,
1597@@ -808,6 +813,42 @@
1598 transaction.commit()
1599
1600
1601+class BugTaskIncompleteMigrator(TunableLoop):
1602+ """Migrate BugTaskStatus 'INCOMPLETE' to a concrete WITH/WITHOUT value."""
1603+
1604+ maximum_chunk_size = 20000
1605+ minimum_chunk_size = 100
1606+
1607+ def __init__(self, log, abort_time=None, max_heat_age=None):
1608+ super(BugTaskIncompleteMigrator, self).__init__(log, abort_time)
1609+ self.transaction = transaction
1610+ self.total_processed = 0
1611+ self.is_done = False
1612+ self.offset = 0
1613+ self.store = IMasterStore(BugTask)
1614+ self.query = self.store.find(
1615+ (BugTask, Bug),
1616+ BugTask._status == BugTaskStatus.INCOMPLETE,
1617+ BugTask.bugID == Bug.id)
1618+
1619+ def isDone(self):
1620+ """See `ITunableLoop`."""
1621+ return self.query.is_empty()
1622+
1623+ def __call__(self, chunk_size):
1624+ """See `ITunableLoop`."""
1625+ transaction.begin()
1626+ tasks = list(self.query[:chunk_size])
1627+ for (task, bug) in tasks:
1628+ if (bug.date_last_message is None or
1629+ task.date_incomplete > bug.date_last_message):
1630+ task._status = BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE
1631+ else:
1632+ task._status = BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE
1633+ self.log.debug("Updated status on %d tasks" % len(tasks))
1634+ transaction.commit()
1635+
1636+
1637 class BugWatchActivityPruner(BulkPruner):
1638 """A TunableLoop to prune BugWatchActivity entries."""
1639 target_table_class = BugWatchActivity
1640@@ -1270,6 +1311,7 @@
1641 BugHeatUpdater,
1642 SourcePackagePublishingHistorySPNPopulator,
1643 BinaryPackagePublishingHistoryBPNPopulator,
1644+ BugTaskIncompleteMigrator,
1645 ]
1646 experimental_tunable_loops = []
1647
1648
1649=== modified file 'lib/lp/scripts/tests/test_garbo.py'
1650--- lib/lp/scripts/tests/test_garbo.py 2011-10-03 09:38:06 +0000
1651+++ lib/lp/scripts/tests/test_garbo.py 2011-10-05 18:55:29 +0000
1652@@ -66,10 +66,15 @@
1653 ZopelessDatabaseLayer,
1654 )
1655 from lp.answers.model.answercontact import AnswerContact
1656+from lp.bugs.interfaces.bugtask import (
1657+ BugTaskStatus,
1658+ BugTaskStatusSearch,
1659+ )
1660 from lp.bugs.model.bugnotification import (
1661 BugNotification,
1662 BugNotificationRecipient,
1663 )
1664+from lp.bugs.model.bugtask import BugTask
1665 from lp.code.bzr import (
1666 BranchFormat,
1667 RepositoryFormat,
1668@@ -858,6 +863,40 @@
1669 self._test_AnswerContactPruner(
1670 AccountStatus.SUSPENDED, ONE_DAY_AGO, expected_count=1)
1671
1672+ def test_BugTaskIncompleteMigrator(self):
1673+ # BugTasks with status INCOMPLETE should be either
1674+ # INCOMPLETE_WITHOUT_RESPONSE or INCOMPLETE_WITH_RESPONSE.
1675+ # Create a bug with two tasks set to INCOMPLETE and a comment between
1676+ # them.
1677+ LaunchpadZopelessLayer.switchDbUser('testadmin')
1678+ store = IMasterStore(BugTask)
1679+ bug = self.factory.makeBug()
1680+ with_response = bug.bugtasks[0]
1681+ with_response.transitionToStatus(BugTaskStatus.INCOMPLETE, bug.owner)
1682+ removeSecurityProxy(with_response)._status = BugTaskStatus.INCOMPLETE
1683+ transaction.commit()
1684+ self.factory.makeBugComment(bug=bug)
1685+ transaction.commit()
1686+ without_response = self.factory.makeBugTask(bug=bug)
1687+ without_response.transitionToStatus(
1688+ BugTaskStatus.INCOMPLETE, bug.owner)
1689+ removeSecurityProxy(without_response)._status = (
1690+ BugTaskStatus.INCOMPLETE)
1691+ transaction.commit()
1692+ self.runHourly()
1693+ self.assertEqual(
1694+ 1,
1695+ store.find(BugTask.id,
1696+ BugTask.id == with_response.id,
1697+ BugTask._status ==
1698+ BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE).count())
1699+ self.assertEqual(
1700+ 1,
1701+ store.find(BugTask.id,
1702+ BugTask.id == without_response.id,
1703+ BugTask._status ==
1704+ BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE).count())
1705+
1706 def test_BranchJobPruner(self):
1707 # Garbo should remove jobs completed over 30 days ago.
1708 LaunchpadZopelessLayer.switchDbUser('testadmin')