Merge lp:~bac/launchpad/bug-759467 into lp:launchpad
- bug-759467
- Merge into devel
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 |
Related bugs: |
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_
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_
INCOMPLETE_
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:/
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/
lib/canonical
lib/lp/
lib/canonical
lib/lp/
lib/lp/
lib/lp/
lib/lp/
database/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/canonical
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
./lib/canonical
1: narrative uses a moin header.
103: want exceeds 78 characters.
114: want exceeds 78 characters.
127: narrative has trailing whitespace.
./lib/lp/
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
592: Line exceeds 78 characters.
./lib/lp/
1: narrative uses a moin header.
14: narrative uses a moin header.
./lib/lp/
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
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_
[3] Done.
[4] The store.flush() was removed.
[5] Much nicer, thanks.
[6] Ditto.
Preview Diff
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') |
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) DBEnumeratedTyp e): FooType, exclude=('TWO')) schema= [FooType, BarType], default=DEFAULT)
139 +schemas into one table. This works if you tell the column about both enums:
140 +
141 + >>> class BarType(
142 + ... use_template(
143 + ... THREE = DBItem(3, "Three")
144 +
145 +Redefine the table with awareness of BarType:
146 +
147 + >>> class FooTest(SQLBase):
148 + ... foo = EnumCol(
This is really cool functionality. At least, it is if you're a nerd like me.
[2]
681 + if new_status in (BugTaskStatusS earch.INCOMPLET E_WITHOUT_ RESPONSE, arch.INCOMPLETE _WITH_RESPONSE)
682 + BugTaskStatusSe
It might be worth adding a INCOMPLETE_ BUGTASK_ STATUSES constant to interfaces. bugtask, just to make this easier. It may also be worth INCOMPLETE to that constant, but I don't know if that
lp.bugs.
adding BugTaskStatus.
would break anything.
[3] find((BugTask, Bug),
1247 + self.query = self.store.
The (BugTask, Bug) tuple should be on a new line here.
[4]
1315 + store.flush() commit( )
1316 + transaction.
Are both of these necessary?
[5]
1322 + removeSecurityP roxy( response) ._status = BugTaskStatus. INCOMPLETE
1323 + without_
A nitpick, but it's generally preferred to write these things thus:
[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.