Merge lp:~lifeless/launchpad/bug-759467 into lp:launchpad
- bug-759467
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Robert Collins |
Approved revision: | no longer in the source branch. |
Merged at revision: | 14106 |
Proposed branch: | lp:~lifeless/launchpad/bug-759467 |
Merge into: | lp:launchpad |
Diff against target: |
917 lines (+268/-120) 19 files modified
database/schema/security.cfg (+1/-0) lib/canonical/config/schema-lazr.conf (+1/-1) lib/canonical/database/enumcol.py (+29/-12) lib/canonical/launchpad/doc/enumcol.txt (+35/-3) lib/lp/bugs/browser/bugtask.py (+1/-52) lib/lp/bugs/browser/tests/bugs-views.txt (+1/-1) lib/lp/bugs/configure.zcml (+1/-0) lib/lp/bugs/doc/bugtask-status-workflow.txt (+1/-1) lib/lp/bugs/interfaces/bugtask.py (+19/-11) lib/lp/bugs/model/bug.py (+10/-2) lib/lp/bugs/model/bugtarget.py (+14/-1) lib/lp/bugs/model/bugtask.py (+69/-30) lib/lp/bugs/model/tests/test_bugtask.py (+2/-0) lib/lp/bugs/model/tests/test_bugtask_status.py (+1/-1) lib/lp/bugs/scripts/tests/test_bugimport.py (+1/-1) lib/lp/registry/model/distribution.py (+2/-2) lib/lp/registry/model/distributionsourcepackage.py (+2/-2) lib/lp/scripts/garbo.py (+41/-0) lib/lp/scripts/tests/test_garbo.py (+37/-0) |
To merge this branch: | bzr merge lp:~lifeless/launchpad/bug-759467 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gavin Panella (community) | Approve | ||
Review via email: mp+58262@code.launchpad.net |
Commit message
[r=allenap][bug=759467] Change from storing INCOMPLETE to storing INCOMPLETE_
Description of the change
Change from storing INCOMPLETE to storing INCOMPLETE_
This required:
- querying on both the current model and the new
- extending enumcol to permit using both schemas
- changing the BugTaskStatusSearch enum values to be intrinsically in the right sort order.
- adding a migration task to the garbo
- making BugTask.status a property
- translating INCOMPLETE into INCOMPLETE_
- changing any INCOMPLETE[
We don't appear to filter on INCOMPLETE in non-nodowntime services so this can go directly into devel.
As a bonus I found some dead code to delete.
Robert Collins (lifeless) wrote : | # |
Gavin Panella (allenap) wrote : | # |
Looks good.
- status = EnumCol(
+ _status = EnumCol(
Did you consider subclassing EnumCol instead of doing this?
Also, any other places that want to select with BugTask.status are
going to access an underscore property. It might have been clearer to
call it db_status or something like that.
Robert Collins (lifeless) wrote : | # |
On the subclassing - I would have needed to change many things;
- the ability to check N schemas
- the policy about which schemas to return (the way we fold things onto INCOMPLETE)
- add a backdoor to get the unmapped value (for code that cares e.g. the migrator)
- add a policy for setting to decide when to permit and when to fold
The N schemas thing could be pulled up as I did into the base class; having done that a property neatly solves all the other issues with ~3 lines of code.
As for the name, We do try pretty hard to only query using BugTaskSet.search because of all the preloading complexities, so I'm not to worried. I'll happily land a s/_/db_/ in future if it starts to gross folk out :).
Thanks for the review!
Preview Diff
1 | === modified file 'database/schema/security.cfg' |
2 | --- database/schema/security.cfg 2011-04-19 12:20:36 +0000 |
3 | +++ database/schema/security.cfg 2011-04-21 04:03:32 +0000 |
4 | @@ -2238,6 +2238,7 @@ |
5 | public.bugnotificationfilter = SELECT, DELETE |
6 | public.bugnotificationrecipientarchive = SELECT |
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-04-13 05:03:04 +0000 |
15 | +++ lib/canonical/config/schema-lazr.conf 2011-04-21 04:03:32 +0000 |
16 | @@ -1489,7 +1489,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-04-21 04:03:32 +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,9 +75,7 @@ |
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 | } |
96 | |
97 | === modified file 'lib/canonical/launchpad/doc/enumcol.txt' |
98 | --- lib/canonical/launchpad/doc/enumcol.txt 2009-04-17 10:32:16 +0000 |
99 | +++ lib/canonical/launchpad/doc/enumcol.txt 2011-04-21 04:03:32 +0000 |
100 | @@ -25,7 +25,7 @@ |
101 | Attempting to use a normal enumerated type for an enumcol will |
102 | result in an error. |
103 | |
104 | - >>> from lazr.enum import EnumeratedType, Item |
105 | + >>> from lazr.enum import EnumeratedType, Item, use_template |
106 | >>> class PlainFooType(EnumeratedType): |
107 | ... """Enumerated type for the foo column.""" |
108 | ... ONE = Item("One") |
109 | @@ -100,7 +100,7 @@ |
110 | >>> t.foo = AnotherType.ONE |
111 | Traceback (most recent call last): |
112 | ... |
113 | - TypeError: DBItem from wrong type, 'FooType' != 'AnotherType' |
114 | + TypeError: DBItem from unknown enum, 'AnotherType' not in (<DBEnumeratedType 'FooType'>,) |
115 | |
116 | The type assigned in must be the exact type, not a derived types. |
117 | |
118 | @@ -111,9 +111,41 @@ |
119 | >>> t.foo = item |
120 | Traceback (most recent call last): |
121 | ... |
122 | - TypeError: DBItem from wrong type, 'FooType' != 'DerivedType' |
123 | + TypeError: DBItem from unknown enum, 'DerivedType' not in (<DBEnumeratedType 'FooType'>,) |
124 | |
125 | A simple way to assign in the correct item is to use the name of the derived |
126 | item to access the correct item from the base type. |
127 | |
128 | >>> t.foo = FooType.items[item.name] |
129 | + |
130 | +Sometimes its useful to serialise things from two different (but related) |
131 | +schemas into one table. This works if you tell the column about both enums: |
132 | + |
133 | + >>> class BarType(DBEnumeratedType): |
134 | + ... use_template(FooType, exclude=('TWO')) |
135 | + ... THREE = DBItem(3, "Three") |
136 | + |
137 | +Redefine the table with awareness of BarType: |
138 | + |
139 | + >>> class FooTest(SQLBase): |
140 | + ... foo = EnumCol(schema=[FooType, BarType], default=DEFAULT) |
141 | + |
142 | +We can assign items from either schema to the table; |
143 | + |
144 | + >>> t = FooTest() |
145 | + >>> t.foo = FooType.ONE |
146 | + >>> t_id = t.id |
147 | + >>> b = FooTest() |
148 | + >>> b.foo = BarType.THREE |
149 | + >>> b_id = b.id |
150 | + |
151 | +And reading back from the database correctly finds things from the schemas in |
152 | +the order given. |
153 | + |
154 | + >>> from storm.store import AutoReload |
155 | + >>> b.foo = AutoReload |
156 | + >>> t.foo = AutoReload |
157 | + >>> b.foo == BarType.THREE |
158 | + True |
159 | + >>> t.foo == FooType.ONE |
160 | + True |
161 | |
162 | === modified file 'lib/lp/bugs/browser/bugtask.py' |
163 | --- lib/lp/bugs/browser/bugtask.py 2011-04-13 11:05:45 +0000 |
164 | +++ lib/lp/bugs/browser/bugtask.py 2011-04-21 04:03:32 +0000 |
165 | @@ -1812,7 +1812,7 @@ |
166 | bug_task_set.buildUpstreamClause(upstream_open_bugs)) |
167 | open_bugs = bug_task_set.open_bugtask_search |
168 | open_bugs.setTarget(self.context) |
169 | - groups = (BugTask.status, BugTask.importance, |
170 | + groups = (BugTask._status, BugTask.importance, |
171 | Bug.latest_patch_uploaded != None, fixed_upstream_clause) |
172 | counts = bug_task_set.countBugs(open_bugs, groups) |
173 | # Sum the split out aggregates. |
174 | @@ -1975,57 +1975,6 @@ |
175 | return search_filter_url |
176 | |
177 | |
178 | -def getInitialValuesFromSearchParams(search_params, form_schema): |
179 | - """Build a dictionary that can be given as initial values to |
180 | - setUpWidgets, based on the given search params. |
181 | - |
182 | - >>> initial = getInitialValuesFromSearchParams( |
183 | - ... {'status': any(*UNRESOLVED_BUGTASK_STATUSES)}, IBugTaskSearch) |
184 | - >>> for status in initial['status']: |
185 | - ... print status.name |
186 | - NEW |
187 | - INCOMPLETE |
188 | - CONFIRMED |
189 | - TRIAGED |
190 | - INPROGRESS |
191 | - FIXCOMMITTED |
192 | - |
193 | - >>> initial = getInitialValuesFromSearchParams( |
194 | - ... {'status': BugTaskStatus.INVALID}, IBugTaskSearch) |
195 | - >>> [status.name for status in initial['status']] |
196 | - ['INVALID'] |
197 | - |
198 | - >>> initial = getInitialValuesFromSearchParams( |
199 | - ... {'importance': [BugTaskImportance.CRITICAL, |
200 | - ... BugTaskImportance.HIGH]}, IBugTaskSearch) |
201 | - >>> [importance.name for importance in initial['importance']] |
202 | - ['CRITICAL', 'HIGH'] |
203 | - |
204 | - >>> getInitialValuesFromSearchParams( |
205 | - ... {'assignee': NULL}, IBugTaskSearch) |
206 | - {'assignee': None} |
207 | - """ |
208 | - initial = {} |
209 | - for key, value in search_params.items(): |
210 | - if IList.providedBy(form_schema[key]): |
211 | - if isinstance(value, any): |
212 | - value = value.query_values |
213 | - elif isinstance(value, (list, tuple)): |
214 | - value = value |
215 | - else: |
216 | - value = [value] |
217 | - elif value == NULL: |
218 | - value = None |
219 | - else: |
220 | - # Should be safe to pass value as it is to setUpWidgets, no need |
221 | - # to worry |
222 | - pass |
223 | - |
224 | - initial[key] = value |
225 | - |
226 | - return initial |
227 | - |
228 | - |
229 | class BugTaskListingItem: |
230 | """A decorated bug task. |
231 | |
232 | |
233 | === modified file 'lib/lp/bugs/browser/tests/bugs-views.txt' |
234 | --- lib/lp/bugs/browser/tests/bugs-views.txt 2010-11-01 15:46:48 +0000 |
235 | +++ lib/lp/bugs/browser/tests/bugs-views.txt 2011-04-21 04:03:32 +0000 |
236 | @@ -21,7 +21,7 @@ |
237 | >>> from lp.bugs.interfaces.bugtask import BugTaskStatus |
238 | >>> from lp.bugs.model.bugtask import BugTask |
239 | >>> [bugtask.bug.id |
240 | - ... for bugtask in BugTask.selectBy(status=BugTaskStatus.FIXRELEASED)] |
241 | + ... for bugtask in BugTask.selectBy(_status=BugTaskStatus.FIXRELEASED)] |
242 | [8] |
243 | >>> for bug in bugs_view.getMostRecentlyFixedBugs(): |
244 | ... print "%s: %s" % (bug.id, bug.title) |
245 | |
246 | === modified file 'lib/lp/bugs/configure.zcml' |
247 | --- lib/lp/bugs/configure.zcml 2011-04-11 12:52:27 +0000 |
248 | +++ lib/lp/bugs/configure.zcml 2011-04-21 04:03:32 +0000 |
249 | @@ -221,6 +221,7 @@ |
250 | distribution |
251 | distroseries |
252 | milestone |
253 | + _status |
254 | status |
255 | statusexplanation |
256 | importance |
257 | |
258 | === modified file 'lib/lp/bugs/doc/bugtask-status-workflow.txt' |
259 | --- lib/lp/bugs/doc/bugtask-status-workflow.txt 2010-10-18 22:24:59 +0000 |
260 | +++ lib/lp/bugs/doc/bugtask-status-workflow.txt 2011-04-21 04:03:32 +0000 |
261 | @@ -57,7 +57,7 @@ |
262 | >>> ubuntu_firefox_task.status = BugTaskStatus.CONFIRMED |
263 | Traceback (most recent call last): |
264 | ... |
265 | - ForbiddenAttribute: ... |
266 | + AttributeError: ... |
267 | |
268 | >>> print ubuntu_firefox_task.status.title |
269 | New |
270 | |
271 | === modified file 'lib/lp/bugs/interfaces/bugtask.py' |
272 | --- lib/lp/bugs/interfaces/bugtask.py 2011-04-12 12:16:03 +0000 |
273 | +++ lib/lp/bugs/interfaces/bugtask.py 2011-04-21 04:03:32 +0000 |
274 | @@ -18,6 +18,7 @@ |
275 | 'BugTaskStatusSearch', |
276 | 'BugTaskStatusSearchDisplay', |
277 | 'ConjoinedBugTaskEditError', |
278 | + 'DB_UNRESOLVED_BUGTASK_STATUSES', |
279 | 'DEFAULT_SEARCH_BUGTASK_STATUSES_FOR_DISPLAY', |
280 | 'IAddBugTaskForm', |
281 | 'IAddBugTaskWithProductCreationForm', |
282 | @@ -201,6 +202,11 @@ |
283 | this product or source package. |
284 | """) |
285 | |
286 | + # INCOMPLETE is never actually stored now: INCOMPLETE_WITH_RESPONSE and |
287 | + # INCOMPLETE_WITHOUT_RESPONSE are mapped to INCOMPLETE on read, and on |
288 | + # write INCOMPLETE is mapped to INCOMPLETE_WITHOUT_RESPONSE. This permits |
289 | + # An index on the INCOMPLETE_WITH*_RESPONSE queries that the webapp |
290 | + # generates. |
291 | INCOMPLETE = DBItem(15, """ |
292 | Incomplete |
293 | |
294 | @@ -274,10 +280,6 @@ |
295 | affected software. |
296 | """) |
297 | |
298 | - # DBItem values 35 and 40 are used by |
299 | - # BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE and |
300 | - # BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE |
301 | - |
302 | UNKNOWN = DBItem(999, """ |
303 | Unknown |
304 | |
305 | @@ -292,19 +294,14 @@ |
306 | """ |
307 | use_template(BugTaskStatus, exclude=('UNKNOWN')) |
308 | |
309 | - sort_order = ( |
310 | - 'NEW', 'INCOMPLETE_WITH_RESPONSE', 'INCOMPLETE_WITHOUT_RESPONSE', |
311 | - 'INCOMPLETE', 'OPINION', 'INVALID', 'WONTFIX', 'EXPIRED', |
312 | - 'CONFIRMED', 'TRIAGED', 'INPROGRESS', 'FIXCOMMITTED', 'FIXRELEASED') |
313 | - |
314 | - INCOMPLETE_WITH_RESPONSE = DBItem(35, """ |
315 | + INCOMPLETE_WITH_RESPONSE = DBItem(13, """ |
316 | Incomplete (with response) |
317 | |
318 | This bug has new information since it was last marked |
319 | as requiring a response. |
320 | """) |
321 | |
322 | - INCOMPLETE_WITHOUT_RESPONSE = DBItem(40, """ |
323 | + INCOMPLETE_WITHOUT_RESPONSE = DBItem(14, """ |
324 | Incomplete (without response) |
325 | |
326 | This bug requires more information, but no additional |
327 | @@ -312,6 +309,7 @@ |
328 | """) |
329 | |
330 | |
331 | + |
332 | class BugTagsSearchCombinator(EnumeratedType): |
333 | """Bug Tags Search Combinator |
334 | |
335 | @@ -376,6 +374,12 @@ |
336 | BugTaskStatus.INPROGRESS, |
337 | BugTaskStatus.FIXCOMMITTED) |
338 | |
339 | +# Actual values stored in the DB: |
340 | +DB_UNRESOLVED_BUGTASK_STATUSES = UNRESOLVED_BUGTASK_STATUSES + ( |
341 | + BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE, |
342 | + BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE, |
343 | + ) |
344 | + |
345 | RESOLVED_BUGTASK_STATUSES = ( |
346 | BugTaskStatus.FIXRELEASED, |
347 | BugTaskStatus.OPINION, |
348 | @@ -492,9 +496,13 @@ |
349 | # bugwatch; this would be better described in a separate interface, |
350 | # but adding a marker interface during initialization is expensive, |
351 | # and adding it post-initialization is not trivial. |
352 | + # Note that status is a property because the model only exposes INCOMPLETE |
353 | + # but the DB stores INCOMPLETE_WITH_RESPONSE and |
354 | + # INCOMPLETE_WITHOUT_RESPONSE for query efficiency. |
355 | status = exported( |
356 | Choice(title=_('Status'), vocabulary=BugTaskStatus, |
357 | default=BugTaskStatus.NEW, readonly=True)) |
358 | + _status = Attribute('The actual status DB column used in queries.') |
359 | importance = exported( |
360 | Choice(title=_('Importance'), vocabulary=BugTaskImportance, |
361 | default=BugTaskImportance.UNDECIDED, readonly=True)) |
362 | |
363 | === modified file 'lib/lp/bugs/model/bug.py' |
364 | --- lib/lp/bugs/model/bug.py 2011-04-13 20:55:31 +0000 |
365 | +++ lib/lp/bugs/model/bug.py 2011-04-21 04:03:32 +0000 |
366 | @@ -154,9 +154,10 @@ |
367 | from lp.bugs.interfaces.bugnotification import IBugNotificationSet |
368 | from lp.bugs.interfaces.bugtask import ( |
369 | BugTaskStatus, |
370 | + BugTaskStatusSearch, |
371 | + DB_UNRESOLVED_BUGTASK_STATUSES, |
372 | IBugTask, |
373 | IBugTaskSet, |
374 | - UNRESOLVED_BUGTASK_STATUSES, |
375 | ) |
376 | from lp.bugs.interfaces.bugtracker import BugTrackerType |
377 | from lp.bugs.interfaces.bugwatch import IBugWatchSet |
378 | @@ -272,7 +273,7 @@ |
379 | Join(BugTask, BugTask.bugID == BugTag.bugID), |
380 | ) |
381 | where_conditions = [ |
382 | - BugTask.status.is_in(UNRESOLVED_BUGTASK_STATUSES), |
383 | + BugTask._status.is_in(DB_UNRESOLVED_BUGTASK_STATUSES), |
384 | context_condition, |
385 | ] |
386 | if wanted_tags is not None: |
387 | @@ -1136,6 +1137,13 @@ |
388 | getUtility(IBugWatchSet).fromText( |
389 | message.text_contents, self, user) |
390 | self.findCvesInText(message.text_contents, user) |
391 | + for bugtask in self.bugtasks: |
392 | + # Check the stored value so we don't write to unaltered tasks. |
393 | + if (bugtask._status == BugTaskStatus.INCOMPLETE or |
394 | + bugtask._status == BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE): |
395 | + # This is not a semantic change, so we don't update date |
396 | + # records or send email. |
397 | + bugtask._status = BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE |
398 | # XXX 2008-05-27 jamesh: |
399 | # Ensure that BugMessages get flushed in same order as |
400 | # they are created. |
401 | |
402 | === modified file 'lib/lp/bugs/model/bugtarget.py' |
403 | --- lib/lp/bugs/model/bugtarget.py 2011-03-29 15:59:22 +0000 |
404 | +++ lib/lp/bugs/model/bugtarget.py 2011-04-21 04:03:32 +0000 |
405 | @@ -48,6 +48,7 @@ |
406 | BugTaskImportance, |
407 | BugTaskSearchParams, |
408 | BugTaskStatus, |
409 | + BugTaskStatusSearch, |
410 | RESOLVED_BUGTASK_STATUSES, |
411 | UNRESOLVED_BUGTASK_STATUSES, |
412 | ) |
413 | @@ -213,6 +214,9 @@ |
414 | if statuses is None: |
415 | statuses = BugTaskStatus.items |
416 | statuses = list(statuses) |
417 | + if BugTaskStatus.INCOMPLETE in statuses: |
418 | + statuses.extend([BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE, |
419 | + BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE]) |
420 | |
421 | count_column = """ |
422 | COUNT (CASE WHEN BugTask.status = %s |
423 | @@ -232,7 +236,16 @@ |
424 | "SELECT %s FROM BugTask, Bug WHERE %s" % ( |
425 | ', '.join(select_columns), ' AND '.join(conditions))) |
426 | counts = cur.fetchone() |
427 | - return dict(zip(statuses, counts)) |
428 | + result = dict(zip(statuses, counts)) |
429 | + def fold_status(from_st, to_st): |
430 | + if from_st in result: |
431 | + result[to_st] = result.get(to_st, 0) + result[from_st] |
432 | + del result[from_st] |
433 | + fold_status(BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE, |
434 | + BugTaskStatus.INCOMPLETE) |
435 | + fold_status(BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE, |
436 | + BugTaskStatus.INCOMPLETE) |
437 | + return result |
438 | |
439 | def getBugTaskWeightFunction(self): |
440 | """Default weight function is the simple one.""" |
441 | |
442 | === modified file 'lib/lp/bugs/model/bugtask.py' |
443 | --- lib/lp/bugs/model/bugtask.py 2011-04-11 12:52:27 +0000 |
444 | +++ lib/lp/bugs/model/bugtask.py 2011-04-21 04:03:32 +0000 |
445 | @@ -113,6 +113,7 @@ |
446 | BugTaskStatus, |
447 | BugTaskStatusSearch, |
448 | ConjoinedBugTaskEditError, |
449 | + DB_UNRESOLVED_BUGTASK_STATUSES, |
450 | IBugTask, |
451 | IBugTaskDelta, |
452 | IBugTaskSet, |
453 | @@ -475,7 +476,7 @@ |
454 | _defaultOrder = ['distribution', 'product', 'productseries', |
455 | 'distroseries', 'milestone', 'sourcepackagename'] |
456 | _CONJOINED_ATTRIBUTES = ( |
457 | - "status", "importance", "assigneeID", "milestoneID", |
458 | + "_status", "importance", "assigneeID", "milestoneID", |
459 | "date_assigned", "date_confirmed", "date_inprogress", |
460 | "date_closed", "date_incomplete", "date_left_new", |
461 | "date_triaged", "date_fix_committed", "date_fix_released", |
462 | @@ -507,9 +508,9 @@ |
463 | dbName='milestone', foreignKey='Milestone', |
464 | notNull=False, default=None, |
465 | storm_validator=validate_conjoined_attribute) |
466 | - status = EnumCol( |
467 | + _status = EnumCol( |
468 | dbName='status', notNull=True, |
469 | - schema=BugTaskStatus, |
470 | + schema=(BugTaskStatus, BugTaskStatusSearch), |
471 | default=BugTaskStatus.NEW, |
472 | storm_validator=validate_status) |
473 | statusexplanation = StringCol(dbName='statusexplanation', default=None) |
474 | @@ -561,6 +562,13 @@ |
475 | dbName='targetnamecache', notNull=False, default=None) |
476 | |
477 | @property |
478 | + def status(self): |
479 | + if (self._status == BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE or |
480 | + self._status == BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE): |
481 | + return BugTaskStatus.INCOMPLETE |
482 | + return self._status |
483 | + |
484 | + @property |
485 | def title(self): |
486 | """See `IBugTask`.""" |
487 | return 'Bug #%s in %s: "%s"' % ( |
488 | @@ -843,12 +851,21 @@ |
489 | "Only Bug Supervisors may change status to %s." % ( |
490 | new_status.title,)) |
491 | |
492 | - if self.status == new_status: |
493 | + if new_status == BugTaskStatus.INCOMPLETE: |
494 | + # We store INCOMPLETE as INCOMPLETE_WITHOUT_RESPONSE so that it can |
495 | + # be queried on efficiently. |
496 | + if (when is None or self.bug.date_last_message is None or |
497 | + when > self.bug.date_last_message): |
498 | + new_status = BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE |
499 | + else: |
500 | + new_status = BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE |
501 | + |
502 | + if self._status == new_status: |
503 | # No change in the status, so nothing to do. |
504 | return |
505 | |
506 | old_status = self.status |
507 | - self.status = new_status |
508 | + self._status = new_status |
509 | |
510 | if new_status == BugTaskStatus.UNKNOWN: |
511 | # Ensure that all status-related dates are cleared, |
512 | @@ -922,17 +939,18 @@ |
513 | # Bugs can jump in and out of 'incomplete' status |
514 | # and for just as long as they're marked incomplete |
515 | # we keep a date_incomplete recorded for them. |
516 | - if new_status == BugTaskStatus.INCOMPLETE: |
517 | + if new_status in (BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE, |
518 | + BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE): |
519 | self.date_incomplete = when |
520 | else: |
521 | self.date_incomplete = None |
522 | |
523 | - if ((old_status in UNRESOLVED_BUGTASK_STATUSES) and |
524 | + if ((old_status in DB_UNRESOLVED_BUGTASK_STATUSES) and |
525 | (new_status in RESOLVED_BUGTASK_STATUSES)): |
526 | self.date_closed = when |
527 | |
528 | if ((old_status in RESOLVED_BUGTASK_STATUSES) and |
529 | - (new_status in UNRESOLVED_BUGTASK_STATUSES)): |
530 | + (new_status in DB_UNRESOLVED_BUGTASK_STATUSES)): |
531 | self.date_left_closed = when |
532 | |
533 | # Ensure that we don't have dates recorded for state |
534 | @@ -940,7 +958,7 @@ |
535 | # workflow state. We want to ensure that, for example, a |
536 | # bugtask that went New => Confirmed => New |
537 | # has a dateconfirmed value of None. |
538 | - if new_status in UNRESOLVED_BUGTASK_STATUSES: |
539 | + if new_status in DB_UNRESOLVED_BUGTASK_STATUSES: |
540 | self.date_closed = None |
541 | |
542 | if new_status < BugTaskStatus.CONFIRMED: |
543 | @@ -1601,29 +1619,45 @@ |
544 | elif zope_isinstance(status, not_equals): |
545 | return '(NOT %s)' % self._buildStatusClause(status.value) |
546 | elif zope_isinstance(status, BaseItem): |
547 | + incomplete_response = ( |
548 | + status == BugTaskStatus.INCOMPLETE) |
549 | with_response = ( |
550 | status == BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE) |
551 | without_response = ( |
552 | status == BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE) |
553 | + # TODO: bug 759467 tracks the migration of INCOMPLETE in the db to |
554 | + # INCOMPLETE_WITH_RESPONSE and INCOMPLETE_WITHOUT_RESPONSE. When |
555 | + # the migration is complete, we can convert status lookups to a |
556 | + # simple IN clause. |
557 | if with_response or without_response: |
558 | - status_clause = ( |
559 | - '(BugTask.status = %s) ' % |
560 | - sqlvalues(BugTaskStatus.INCOMPLETE)) |
561 | if with_response: |
562 | - status_clause += (""" |
563 | + return """( |
564 | + BugTask.status = %s OR |
565 | + (BugTask.status = %s |
566 | AND (Bug.date_last_message IS NOT NULL |
567 | AND BugTask.date_incomplete <= |
568 | - Bug.date_last_message) |
569 | - """) |
570 | + Bug.date_last_message))) |
571 | + """ % sqlvalues( |
572 | + BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE, |
573 | + BugTaskStatus.INCOMPLETE) |
574 | elif without_response: |
575 | - status_clause += (""" |
576 | + return """( |
577 | + BugTask.status = %s OR |
578 | + (BugTask.status = %s |
579 | AND (Bug.date_last_message IS NULL |
580 | OR BugTask.date_incomplete > |
581 | - Bug.date_last_message) |
582 | - """) |
583 | - else: |
584 | - assert with_response != without_response |
585 | - return status_clause |
586 | + Bug.date_last_message))) |
587 | + """ % sqlvalues( |
588 | + BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE, |
589 | + BugTaskStatus.INCOMPLETE) |
590 | + assert with_response != without_response |
591 | + elif incomplete_response: |
592 | + # search for any of INCOMPLETE (being migrated from), |
593 | + # INCOMPLETE_WITH_RESPONSE or INCOMPLETE_WITHOUT_RESPONSE |
594 | + return 'BugTask.status %s' % search_value_to_where_condition( |
595 | + any(BugTaskStatus.INCOMPLETE, |
596 | + BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE, |
597 | + BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE)) |
598 | else: |
599 | return '(BugTask.status = %s)' % sqlvalues(status) |
600 | else: |
601 | @@ -1660,7 +1694,7 @@ |
602 | And(ConjoinedMaster.bugID == BugTask.bugID, |
603 | BugTask.distributionID == milestone.distribution.id, |
604 | ConjoinedMaster.distroseriesID == current_series.id, |
605 | - Not(ConjoinedMaster.status.is_in( |
606 | + Not(ConjoinedMaster._status.is_in( |
607 | BugTask._NON_CONJOINED_STATUSES)))) |
608 | join_tables = [(ConjoinedMaster, join)] |
609 | else: |
610 | @@ -1680,7 +1714,7 @@ |
611 | And(ConjoinedMaster.bugID == BugTask.bugID, |
612 | ConjoinedMaster.productseriesID |
613 | == Product.development_focusID, |
614 | - Not(ConjoinedMaster.status.is_in( |
615 | + Not(ConjoinedMaster._status.is_in( |
616 | BugTask._NON_CONJOINED_STATUSES)))), |
617 | ] |
618 | # join.right is the table name. |
619 | @@ -1693,7 +1727,7 @@ |
620 | And(ConjoinedMaster.bugID == BugTask.bugID, |
621 | BugTask.productID == milestone.product.id, |
622 | ConjoinedMaster.productseriesID == dev_focus_id, |
623 | - Not(ConjoinedMaster.status.is_in( |
624 | + Not(ConjoinedMaster._status.is_in( |
625 | BugTask._NON_CONJOINED_STATUSES)))) |
626 | join_tables = [(ConjoinedMaster, join)] |
627 | else: |
628 | @@ -2190,6 +2224,8 @@ |
629 | statuses_for_open_tasks = [ |
630 | BugTaskStatus.NEW, |
631 | BugTaskStatus.INCOMPLETE, |
632 | + BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE, |
633 | + BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE, |
634 | BugTaskStatus.CONFIRMED, |
635 | BugTaskStatus.INPROGRESS, |
636 | BugTaskStatus.UNKNOWN] |
637 | @@ -2601,7 +2637,7 @@ |
638 | |
639 | non_target_create_params = dict( |
640 | bug=bug, |
641 | - status=status, |
642 | + _status=status, |
643 | importance=importance, |
644 | assignee=assignee, |
645 | owner=owner, |
646 | @@ -2736,14 +2772,16 @@ |
647 | """ + target_clause + """ |
648 | """ + bug_clause + """ |
649 | """ + bug_privacy_filter + """ |
650 | - AND BugTask.status = %s |
651 | + AND BugTask.status in (%s, %s, %s) |
652 | AND BugTask.assignee IS NULL |
653 | AND BugTask.milestone IS NULL |
654 | AND Bug.duplicateof IS NULL |
655 | AND Bug.date_last_updated < CURRENT_TIMESTAMP |
656 | AT TIME ZONE 'UTC' - interval '%s days' |
657 | AND BugWatch.id IS NULL |
658 | - )""" % sqlvalues(BugTaskStatus.INCOMPLETE, min_days_old) |
659 | + )""" % sqlvalues(BugTaskStatus.INCOMPLETE, |
660 | + BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE, |
661 | + BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE, min_days_old) |
662 | expirable_bugtasks = BugTask.select( |
663 | query + unconfirmed_bug_condition, |
664 | clauseTables=['Bug'], |
665 | @@ -2761,6 +2799,7 @@ |
666 | """ |
667 | statuses_not_preventing_expiration = [ |
668 | BugTaskStatus.INVALID, BugTaskStatus.INCOMPLETE, |
669 | + BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE, |
670 | BugTaskStatus.WONTFIX] |
671 | |
672 | unexpirable_status_list = [ |
673 | @@ -2906,7 +2945,7 @@ |
674 | ] |
675 | |
676 | product_ids = [product.id for product in products] |
677 | - conditions = And(BugTask.status.is_in(UNRESOLVED_BUGTASK_STATUSES), |
678 | + conditions = And(BugTask._status.is_in(DB_UNRESOLVED_BUGTASK_STATUSES), |
679 | Bug.duplicateof == None, |
680 | BugTask.productID.is_in(product_ids)) |
681 | |
682 | @@ -2934,7 +2973,7 @@ |
683 | # TODO: sort by their name? |
684 | "assignee": BugTask.assigneeID, |
685 | "targetname": BugTask.targetnamecache, |
686 | - "status": BugTask.status, |
687 | + "status": BugTask._status, |
688 | "title": Bug.title, |
689 | "milestone": BugTask.milestoneID, |
690 | "dateassigned": BugTask.date_assigned, |
691 | @@ -3041,7 +3080,7 @@ |
692 | |
693 | open_bugs_cond = ( |
694 | 'BugTask.status %s' % search_value_to_where_condition( |
695 | - any(*UNRESOLVED_BUGTASK_STATUSES))) |
696 | + any(*DB_UNRESOLVED_BUGTASK_STATUSES))) |
697 | |
698 | sum_template = "SUM(CASE WHEN %s THEN 1 ELSE 0 END) AS %s" |
699 | sums = [ |
700 | |
701 | === modified file 'lib/lp/bugs/model/tests/test_bugtask.py' |
702 | --- lib/lp/bugs/model/tests/test_bugtask.py 2011-04-11 12:52:27 +0000 |
703 | +++ lib/lp/bugs/model/tests/test_bugtask.py 2011-04-21 04:03:32 +0000 |
704 | @@ -30,6 +30,7 @@ |
705 | BugTaskImportance, |
706 | BugTaskSearchParams, |
707 | BugTaskStatus, |
708 | + DB_UNRESOLVED_BUGTASK_STATUSES, |
709 | IBugTaskSet, |
710 | IUpstreamBugTask, |
711 | RESOLVED_BUGTASK_STATUSES, |
712 | @@ -1353,6 +1354,7 @@ |
713 | """ |
714 | self.assertNotIn(BugTaskStatus.UNKNOWN, RESOLVED_BUGTASK_STATUSES) |
715 | self.assertNotIn(BugTaskStatus.UNKNOWN, UNRESOLVED_BUGTASK_STATUSES) |
716 | + self.assertNotIn(BugTaskStatus.UNKNOWN, DB_UNRESOLVED_BUGTASK_STATUSES) |
717 | |
718 | |
719 | class TestBugTaskContributor(TestCaseWithFactory): |
720 | |
721 | === modified file 'lib/lp/bugs/model/tests/test_bugtask_status.py' |
722 | --- lib/lp/bugs/model/tests/test_bugtask_status.py 2011-03-23 16:28:51 +0000 |
723 | +++ lib/lp/bugs/model/tests/test_bugtask_status.py 2011-04-21 04:03:32 +0000 |
724 | @@ -181,7 +181,7 @@ |
725 | team = self.factory.makeTeam(members=[self.reporter]) |
726 | team_bug = self.factory.makeBug(owner=team) |
727 | naked_task = removeSecurityProxy(team_bug.default_bugtask) |
728 | - naked_task.status = BugTaskStatus.FIXRELEASED |
729 | + naked_task._status = BugTaskStatus.FIXRELEASED |
730 | with person_logged_in(self.reporter): |
731 | team_bug.default_bugtask.transitionToStatus( |
732 | BugTaskStatus.CONFIRMED, self.reporter) |
733 | |
734 | === modified file 'lib/lp/bugs/scripts/tests/test_bugimport.py' |
735 | --- lib/lp/bugs/scripts/tests/test_bugimport.py 2010-12-16 17:01:06 +0000 |
736 | +++ lib/lp/bugs/scripts/tests/test_bugimport.py 2011-04-21 04:03:32 +0000 |
737 | @@ -845,7 +845,7 @@ |
738 | if bugtask.conjoined_master is not None: |
739 | continue |
740 | bugtask = removeSecurityProxy(bugtask) |
741 | - bugtask.status = new_malone_status |
742 | + bugtask._status = new_malone_status |
743 | if self.failing: |
744 | cur = cursor() |
745 | cur.execute(""" |
746 | |
747 | === modified file 'lib/lp/registry/model/distribution.py' |
748 | --- lib/lp/registry/model/distribution.py 2011-04-15 15:08:20 +0000 |
749 | +++ lib/lp/registry/model/distribution.py 2011-04-21 04:03:32 +0000 |
750 | @@ -106,7 +106,7 @@ |
751 | from lp.bugs.interfaces.bugtarget import IHasBugHeat |
752 | from lp.bugs.interfaces.bugtask import ( |
753 | BugTaskStatus, |
754 | - UNRESOLVED_BUGTASK_STATUSES, |
755 | + DB_UNRESOLVED_BUGTASK_STATUSES, |
756 | ) |
757 | from lp.bugs.interfaces.bugtaskfilter import OrderedBugTask |
758 | from lp.bugs.model.bug import ( |
759 | @@ -1720,7 +1720,7 @@ |
760 | 'triaged': quote(BugTaskStatus.TRIAGED), |
761 | 'limit': limit, |
762 | 'distro': self.id, |
763 | - 'unresolved': quote(UNRESOLVED_BUGTASK_STATUSES), |
764 | + 'unresolved': quote(DB_UNRESOLVED_BUGTASK_STATUSES), |
765 | 'excluded_packages': quote(exclude_packages), |
766 | }) |
767 | counts = cur.fetchall() |
768 | |
769 | === modified file 'lib/lp/registry/model/distributionsourcepackage.py' |
770 | --- lib/lp/registry/model/distributionsourcepackage.py 2011-04-12 06:21:39 +0000 |
771 | +++ lib/lp/registry/model/distributionsourcepackage.py 2011-04-21 04:03:32 +0000 |
772 | @@ -40,7 +40,7 @@ |
773 | from canonical.lazr.utils import smartquote |
774 | from lp.answers.interfaces.questiontarget import IQuestionTarget |
775 | from lp.bugs.interfaces.bugtarget import IHasBugHeat |
776 | -from lp.bugs.interfaces.bugtask import UNRESOLVED_BUGTASK_STATUSES |
777 | +from lp.bugs.interfaces.bugtask import DB_UNRESOLVED_BUGTASK_STATUSES |
778 | from lp.bugs.model.bug import ( |
779 | Bug, |
780 | BugSet, |
781 | @@ -219,7 +219,7 @@ |
782 | BugTask.distributionID == self.distribution.id, |
783 | BugTask.sourcepackagenameID == self.sourcepackagename.id, |
784 | Bug.duplicateof == None, |
785 | - BugTask.status.is_in(UNRESOLVED_BUGTASK_STATUSES)).one() |
786 | + BugTask._status.is_in(DB_UNRESOLVED_BUGTASK_STATUSES)).one() |
787 | |
788 | # Aggregate functions return NULL if zero rows match. |
789 | row = list(row) |
790 | |
791 | === modified file 'lib/lp/scripts/garbo.py' |
792 | --- lib/lp/scripts/garbo.py 2011-04-18 15:40:02 +0000 |
793 | +++ lib/lp/scripts/garbo.py 2011-04-21 04:03:32 +0000 |
794 | @@ -54,9 +54,14 @@ |
795 | MASTER_FLAVOR, |
796 | ) |
797 | from lp.bugs.interfaces.bug import IBugSet |
798 | +from lp.bugs.interfaces.bugtask import ( |
799 | + BugTaskStatus, |
800 | + BugTaskStatusSearch, |
801 | + ) |
802 | from lp.bugs.model.bug import Bug |
803 | from lp.bugs.model.bugattachment import BugAttachment |
804 | from lp.bugs.model.bugnotification import BugNotification |
805 | +from lp.bugs.model.bugtask import BugTask |
806 | from lp.bugs.model.bugwatch import BugWatchActivity |
807 | from lp.bugs.scripts.checkwatches.scheduler import ( |
808 | BugWatchScheduler, |
809 | @@ -730,6 +735,41 @@ |
810 | transaction.commit() |
811 | |
812 | |
813 | +class BugTaskIncompleteMigrator(TunableLoop): |
814 | + """Migrate BugTaskStatus 'INCOMPLETE' to a concrete WITH/WITHOUT value.""" |
815 | + |
816 | + maximum_chunk_size = 20000 |
817 | + minimum_chunk_size = 100 |
818 | + |
819 | + def __init__(self, log, abort_time=None, max_heat_age=None): |
820 | + super(BugTaskIncompleteMigrator, self).__init__(log, abort_time) |
821 | + self.transaction = transaction |
822 | + self.total_processed = 0 |
823 | + self.is_done = False |
824 | + self.offset = 0 |
825 | + self.store = IMasterStore(BugTask) |
826 | + self.query = self.store.find((BugTask, Bug), |
827 | + BugTask._status==BugTaskStatus.INCOMPLETE, |
828 | + BugTask.bugID==Bug.id) |
829 | + |
830 | + def isDone(self): |
831 | + """See `ITunableLoop`.""" |
832 | + return self.query.is_empty() |
833 | + |
834 | + def __call__(self, chunk_size): |
835 | + """See `ITunableLoop`.""" |
836 | + transaction.begin() |
837 | + tasks = list(self.query[:chunk_size]) |
838 | + for (task, bug) in tasks: |
839 | + if (bug.date_last_message is None or |
840 | + task.date_incomplete > bug.date_last_message): |
841 | + task._status = BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE |
842 | + else: |
843 | + task._status = BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE |
844 | + self.log.debug("Updated status on %d tasks" % len(tasks)) |
845 | + transaction.commit() |
846 | + |
847 | + |
848 | class BugWatchActivityPruner(BulkPruner): |
849 | """A TunableLoop to prune BugWatchActivity entries.""" |
850 | target_table_class = BugWatchActivity |
851 | @@ -1050,6 +1090,7 @@ |
852 | UnusedSessionPruner, |
853 | DuplicateSessionPruner, |
854 | BugHeatUpdater, |
855 | + BugTaskIncompleteMigrator, |
856 | ] |
857 | experimental_tunable_loops = [] |
858 | |
859 | |
860 | === modified file 'lib/lp/scripts/tests/test_garbo.py' |
861 | --- lib/lp/scripts/tests/test_garbo.py 2011-04-15 04:56:44 +0000 |
862 | +++ lib/lp/scripts/tests/test_garbo.py 2011-04-21 04:03:32 +0000 |
863 | @@ -52,10 +52,15 @@ |
864 | LaunchpadZopelessLayer, |
865 | ZopelessDatabaseLayer, |
866 | ) |
867 | +from lp.bugs.interfaces.bugtask import ( |
868 | + BugTaskStatus, |
869 | + BugTaskStatusSearch, |
870 | + ) |
871 | from lp.bugs.model.bugnotification import ( |
872 | BugNotification, |
873 | BugNotificationRecipient, |
874 | ) |
875 | +from lp.bugs.model.bugtask import BugTask |
876 | from lp.code.bzr import ( |
877 | BranchFormat, |
878 | RepositoryFormat, |
879 | @@ -768,6 +773,38 @@ |
880 | BugNotification.date_emailed < THIRTY_DAYS_AGO).count(), |
881 | 0) |
882 | |
883 | + def test_BugTaskIncompleteMigrator(self): |
884 | + # BugTasks with status INCOMPLETE should be either |
885 | + # INCOMPLETE_WITHOUT_RESPONSE or INCOMPLETE_WITH_RESPONSE. |
886 | + # Create a bug with two tasks set to INCOMPLETE and a comment between |
887 | + # them. |
888 | + LaunchpadZopelessLayer.switchDbUser('testadmin') |
889 | + store = IMasterStore(BugTask) |
890 | + bug = self.factory.makeBug() |
891 | + with_response = bug.bugtasks[0] |
892 | + with_response.transitionToStatus(BugTaskStatus.INCOMPLETE, bug.owner) |
893 | + removeSecurityProxy(with_response)._status = BugTaskStatus.INCOMPLETE |
894 | + store.flush() |
895 | + transaction.commit() |
896 | + self.factory.makeBugComment(bug=bug) |
897 | + transaction.commit() |
898 | + without_response = self.factory.makeBugTask(bug=bug) |
899 | + without_response.transitionToStatus(BugTaskStatus.INCOMPLETE, bug.owner) |
900 | + removeSecurityProxy(without_response |
901 | + )._status = BugTaskStatus.INCOMPLETE |
902 | + transaction.commit() |
903 | + self.runHourly() |
904 | + self.assertEqual(1, |
905 | + store.find(BugTask.id, |
906 | + BugTask.id==with_response.id, |
907 | + BugTask._status==BugTaskStatusSearch.INCOMPLETE_WITH_RESPONSE |
908 | + ).count()) |
909 | + self.assertEqual(1, |
910 | + store.find(BugTask.id, |
911 | + BugTask.id==without_response.id, |
912 | + BugTask._status== |
913 | + BugTaskStatusSearch.INCOMPLETE_WITHOUT_RESPONSE).count()) |
914 | + |
915 | def test_BranchJobPruner(self): |
916 | # Garbo should remove jobs completed over 30 days ago. |
917 | LaunchpadZopelessLayer.switchDbUser('testadmin') |
I haven't [yet] checked for test fallout beyond the migration and interop stuff; I could possibly be convinced to add extra tests for this behaviour but I figure the current tests are sufficient as the contract isn't altered.